/*
 * Copyright (c) 2016 Advanced Micro Devices, Inc.
 * All rights reserved.
 *
 * For use for simulation and test purposes only
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors: Brandon Potter
 */

#include "sim/fd_array.hh"

#include <fcntl.h>
#include <unistd.h>

#include <array>
#include <memory>
#include <string>

#include "base/logging.hh"
#include "base/output.hh"
#include "params/Process.hh"
#include "sim/fd_entry.hh"

FDArray::FDArray(std::string const& input, std::string const& output,
                 std::string const& errout)
    :  _fdArray(), _input(input), _output(output), _errout(errout),
      _imap {{"",       -1},
             {"cin",    STDIN_FILENO},
             {"stdin",  STDIN_FILENO}},
      _oemap{{"",       -1},
             {"cout",   STDOUT_FILENO},
             {"stdout", STDOUT_FILENO},
             {"cerr",   STDERR_FILENO},
             {"stderr", STDERR_FILENO}}
{
    int sim_fd;
    std::map<std::string, int>::iterator it;

    /**
     * Search through the input options and setup the default fd if match is
     * found; otherwise, open an input file and seek to location.
     */
    if ((it = _imap.find(input)) != _imap.end())
        sim_fd = it->second;
    else
        sim_fd = openInputFile(input);

    auto ffd = std::make_shared<FileFDEntry>(sim_fd, O_RDONLY, input, false);
    _fdArray[STDIN_FILENO] = ffd;

    /**
     * Search through the output/error options and setup the default fd if
     * match is found; otherwise, open an output file and seek to location.
     */
    if ((it = _oemap.find(output)) != _oemap.end())
        sim_fd = it->second;
    else
        sim_fd = openOutputFile(output);

    ffd = std::make_shared<FileFDEntry>(sim_fd, O_WRONLY | O_CREAT | O_TRUNC,
                                        output, false);
    _fdArray[STDOUT_FILENO] = ffd;

    if (output == errout)
        ; /* Reuse the same file descriptor if these match. */
    else if ((it = _oemap.find(errout)) != _oemap.end())
        sim_fd = it->second;
    else
        sim_fd = openOutputFile(errout);

    ffd = std::make_shared<FileFDEntry>(sim_fd, O_WRONLY | O_CREAT | O_TRUNC,
                                        errout, false);
    _fdArray[STDERR_FILENO] = ffd;
}

void
FDArray::updateFileOffsets()
{
    for (auto& fdp : _fdArray) {
        /**
         * It only makes sense to check the offsets if the file descriptor
         * type is 'File' (which indicates that this file is backed by a
         * file on the host). If the type is File, then record the offset.
         */
        auto ffd = std::dynamic_pointer_cast<FileFDEntry>(fdp);

        if (!ffd)
            continue;

        /**
         * Use lseek with SEEK_CUR with offset 0 to figure out where the
         * offset currently resides and pass that back to our setter.
         */
        int sim_fd = ffd->getSimFD();
        ffd->setFileOffset(lseek(sim_fd, 0, SEEK_CUR));
    }
}

void
FDArray::restoreFileOffsets()
{
    /**
     * Use this lambda to highlight what we mean to do with the seek.
     * Notice that this either seeks correctly (sets the file location on the
     * host) or it fails with a fatal. The error is fatal because it's not
     * possible to guarantee that the simulation will proceed as it should
     * have in the same way that it would have proceeded sans checkpoints.
     */
    auto seek = [] (std::shared_ptr<FileFDEntry> ffd)
    {
        if (lseek(ffd->getSimFD(), ffd->getFileOffset(), SEEK_SET) < 0)
            fatal("Unable to seek to location in %s", ffd->getFileName());
    };

    std::map<std::string, int>::iterator it;

    /**
     * Search through the input options and set fd if match is found;
     * otherwise, open an input file and seek to location.
     * Check if user has specified a different input file, and if so, use it
     * instead of the file specified in the checkpoint. This also resets the
     * file offset from the checkpointed value
     */
    std::shared_ptr<FDEntry> stdin_fde = _fdArray[STDIN_FILENO];
    auto stdin_ffd = std::dynamic_pointer_cast<FileFDEntry>(stdin_fde);

    if (_input != stdin_ffd->getFileName()) {
        warn("Using new input file (%s) rather than checkpointed (%s)\n",
             _input, stdin_ffd->getFileName());
        stdin_ffd->setFileName(_input);
        stdin_ffd->setFileOffset(0);
    }

    if ((it = _imap.find(stdin_ffd->getFileName())) != _imap.end()) {
        stdin_ffd->setSimFD(it->second);
    } else {
        stdin_ffd->setSimFD(openInputFile(stdin_ffd->getFileName()));
        seek(stdin_ffd);
    }

    /**
     * Search through the output options and set fd if match is found;
     * otherwise, open an output file and seek to location.
     * Check if user has specified a different output file, and if so, use it
     * instead of the file specified in the checkpoint. This also resets the
     * file offset from the checkpointed value
     */
    std::shared_ptr<FDEntry> stdout_fde = _fdArray[STDOUT_FILENO];
    auto stdout_ffd = std::dynamic_pointer_cast<FileFDEntry>(stdout_fde);

    if (_output != stdout_ffd->getFileName()) {
        warn("Using new output file (%s) rather than checkpointed (%s)\n",
             _output, stdout_ffd->getFileName());
        stdout_ffd->setFileName(_output);
        stdout_ffd->setFileOffset(0);
    }

    if ((it = _oemap.find(stdout_ffd->getFileName())) != _oemap.end()) {
        stdout_ffd->setSimFD(it->second);
    } else {
        stdout_ffd->setSimFD(openOutputFile(stdout_ffd->getFileName()));
        seek(stdout_ffd);
    }

    /**
     * Search through the error options and set fd if match is found;
     * otherwise, open an error file and seek to location.
     * Check if user has specified a different error file, and if so, use it
     * instead of the file specified in the checkpoint. This also resets the
     * file offset from the checkpointed value
     */
    std::shared_ptr<FDEntry> stderr_fde = _fdArray[STDERR_FILENO];
    auto stderr_ffd = std::dynamic_pointer_cast<FileFDEntry>(stderr_fde);

    if (_errout != stderr_ffd->getFileName()) {
        warn("Using new error file (%s) rather than checkpointed (%s)\n",
             _errout, stderr_ffd->getFileName());
        stderr_ffd->setFileName(_errout);
        stderr_ffd->setFileOffset(0);
    }

    if (stdout_ffd->getFileName() == stderr_ffd->getFileName()) {
        /* Reuse the same sim_fd file descriptor if these match. */
        stderr_ffd->setSimFD(stdout_ffd->getSimFD());
    } else if ((it = _oemap.find(stderr_ffd->getFileName())) != _oemap.end()) {
        stderr_ffd->setSimFD(it->second);
    } else {
        stderr_ffd->setSimFD(openOutputFile(stderr_ffd->getFileName()));
        seek(stderr_ffd);
    }

    for (int tgt_fd = 3; tgt_fd < _fdArray.size(); tgt_fd++) {
        std::shared_ptr<FDEntry> fdp = _fdArray[tgt_fd];
        if (!fdp)
            continue;

        /* Need to reconnect pipe ends. */
        if (auto pfd = std::dynamic_pointer_cast<PipeFDEntry>(fdp)) {
            /**
             * Check which end of the pipe we are looking at; we only want
             * to setup the pipe once so we arbitrarily choose the read
             * end to be the end that sets up the pipe.
             */
            if (pfd->getEndType() == PipeFDEntry::EndType::write)
                continue;

            /* Setup the pipe or fatal out of the simulation. */
            int fd_pair[2];
            if (pipe(fd_pair) < 0)
                fatal("Unable to create new pipe");

            /**
             * Reconstruct the ends of the pipe by reassigning the pipe
             * that we created on the host. This one is the read end.
             */
            pfd->setSimFD(fd_pair[0]);

            /**
             * Grab the write end by referencing the read ends source and
             * using that tgt_fd to index the array.
             */
            int prs = pfd->getPipeReadSource();
            std::shared_ptr<FDEntry> write_fdp = _fdArray[prs];

            /* Now cast it and make sure that we are still sane. */
            auto write_pfd = std::dynamic_pointer_cast<PipeFDEntry>(write_fdp);

            /* Hook up the write end back to the right side of the pipe. */
            write_pfd->setSimFD(fd_pair[1]);
        }

        /* Need to reassign 'driver'. */
        if (auto dfd = std::dynamic_pointer_cast<DeviceFDEntry>(fdp)) {
            /**
             * Yeah, how does one retain the entire driver state from this
             * particular set of code? If you figure it out, add some code
             * here to rectify the issue.
             */
            fatal("Unable to restore checkpoints with emulated drivers");
        }

        /* Need to open files and seek. */
        if (auto ffd = std::dynamic_pointer_cast<FileFDEntry>(fdp)) {
            /**
             * Assume that this has the mode of an output file so there's no
             * need to worry about properly recording the mode. If you're
             * reading this and this happens to be your issue, at least be
             * happy that you've discovered the issue (and not mad at me).
             * Onward ho!
             */
            int sim_fd = openFile(ffd->getFileName(), ffd->getFlags(), 0664);
            ffd->setSimFD(sim_fd);
            seek(ffd);
        }
    }
}

int
FDArray::allocFD(std::shared_ptr<FDEntry> in)
{
    for (int i = 0; i < _fdArray.size(); i++) {
        std::shared_ptr<FDEntry> fdp = _fdArray[i];
        if (!fdp) {
            _fdArray[i] = in;
            return i;
        }
    }
    fatal("Out of target file descriptors");
}

int
FDArray::openFile(std::string const& filename, int flags, mode_t mode) const
{
    int sim_fd = open(filename.c_str(), flags, mode);
    if (sim_fd != -1)
        return sim_fd;
    fatal("Unable to open %s with mode %O", filename, mode);
}

int
FDArray::openInputFile(std::string const& filename) const
{
    return openFile(filename, O_RDONLY, 00);
}

int
FDArray::openOutputFile(std::string const& filename) const
{
    return openFile(simout.resolve(filename),
                    O_WRONLY | O_CREAT | O_TRUNC, 0664);
}

std::shared_ptr<FDEntry>
FDArray::getFDEntry(int tgt_fd)
{
    assert(0 <= tgt_fd && tgt_fd < _fdArray.size());
    return _fdArray[tgt_fd];
}

void
FDArray::setFDEntry(int tgt_fd, std::shared_ptr<FDEntry> fdep)
{
    assert(0 <= tgt_fd && tgt_fd < _fdArray.size());
    _fdArray[tgt_fd] = fdep;
}

int
FDArray::closeFDEntry(int tgt_fd)
{
    if (tgt_fd >= _fdArray.size() || tgt_fd < 0)
        return -EBADF;

    int sim_fd = -1;
    auto hbfdp = std::dynamic_pointer_cast<HBFDEntry>(_fdArray[tgt_fd]);
    if (hbfdp)
        sim_fd = hbfdp->getSimFD();

    int status = 0;
    if (sim_fd > 2)
        status = close(sim_fd);

    if (status == 0)
        _fdArray[tgt_fd] = nullptr;

    return status;
}