// Copyright 2021 The Dawn & Tint Authors
//
// 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.

#include <stdio.h>
#include <stdlib.h>
#include <fstream>
#include <iostream>
#include <regex>
#include <sstream>
#include <string>
#include <thread>
#include <type_traits>
#include <vector>

#if TINT_BUILD_MSL_WRITER
#include "src/tint/lang/msl/validate/validate.h"
#endif

#include "src/tint/utils/macros/compiler.h"
#include "src/tint/utils/socket/socket.h"

namespace {

/// The return structure of a compile function
struct CompileResult {
    /// True if shader compiled
    bool success = false;
    /// Output of the compiler
    std::string output;
};

/// Print the tool usage, and exit with 1.
[[noreturn]] void ShowUsage() {
    const char* name = "tint-remote-compile";
    printf(R"(%s is a tool for compiling a shader on a remote machine

usage as server:
  %s -s [-p port-number]

usage as client:
  %s [-p port-number] [server-address] shader-file-path

  [server-address] can be omitted if the TINT_REMOTE_COMPILE_ADDRESS environment
  variable is set.
  Alternatively, you can pass xcrun arguments so %s can be used as a
  drop-in replacement.
)",
           name, name, name, name);
    exit(1);
}

/// The protocol version code. Bump each time the protocol changes
constexpr uint32_t kProtocolVersion = 1;

/// Supported shader source languages
enum SourceLanguage : uint8_t {
    MSL,
};

/// Stream is a serialization wrapper around a socket
struct Stream {
    /// The underlying socket
    tint::socket::Socket* const socket;
    /// Error state
    std::string error;

    /// Writes a uint32_t to the socket
    Stream operator<<(uint32_t v) {
        if (error.empty()) {
            Write(&v, sizeof(v));
        }
        return *this;
    }

    /// Reads a uint32_t from the socket
    Stream operator>>(uint32_t& v) {
        if (error.empty()) {
            Read(&v, sizeof(v));
        }
        return *this;
    }

    /// Writes a std::string to the socket
    Stream operator<<(const std::string& v) {
        if (error.empty()) {
            uint32_t count = static_cast<uint32_t>(v.size());
            *this << count;
            if (count) {
                Write(v.data(), count);
            }
        }
        return *this;
    }

    /// Reads a std::string from the socket
    Stream operator>>(std::string& v) {
        uint32_t count = 0;
        *this >> count;
        if (count) {
            std::vector<char> buf(count);
            if (Read(buf.data(), count)) {
                v = std::string(buf.data(), buf.size());
            }
        } else {
            v.clear();
        }
        return *this;
    }

    /// Writes an enum value to the socket
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, Stream> operator<<(T e) {
        return *this << static_cast<uint32_t>(e);
    }

    /// Reads an enum value from the socket
    template <typename T>
    std::enable_if_t<std::is_enum<T>::value, Stream> operator>>(T& e) {
        uint32_t v;
        *this >> v;
        e = static_cast<T>(v);
        return *this;
    }

  private:
    bool Write(const void* data, size_t size) {
        if (error.empty()) {
            if (!socket->Write(data, size)) {
                error = "Socket::Write() failed";
            }
        }
        return error.empty();
    }

    bool Read(void* data, size_t size) {
        auto buf = reinterpret_cast<uint8_t*>(data);
        while (size > 0 && error.empty()) {
            if (auto n = socket->Read(buf, size)) {
                if (n > size) {
                    error = "Socket::Read() returned more bytes than requested";
                    return false;
                }
                size -= n;
                buf += n;
            } else {
                error = "Socket::Read() failed";
            }
        }
        return error.empty();
    }
};

////////////////////////////////////////////////////////////////////////////////
// Messages
////////////////////////////////////////////////////////////////////////////////

/// Base class for all messages
struct Message {
    /// The type of the message
    enum class Type : uint8_t {
        ConnectionRequest,
        ConnectionResponse,
        CompileRequest,
        CompileResponse,
    };

    explicit Message(Type ty) : type(ty) {}

    const Type type;
};

struct ConnectionResponse : Message {  // Server -> Client
    ConnectionResponse() : Message(Type::ConnectionResponse) {}

    template <typename T>
    void Serialize(T&& f) {
        f(error);
    }

    std::string error;
};

struct ConnectionRequest : Message {  // Client -> Server
    using Response = ConnectionResponse;

    explicit ConnectionRequest(uint32_t proto_ver = kProtocolVersion)
        : Message(Type::ConnectionRequest), protocol_version(proto_ver) {}

    template <typename T>
    void Serialize(T&& f) {
        f(protocol_version);
    }

    uint32_t protocol_version;
};

struct CompileResponse : Message {  //  Server -> Client
    CompileResponse() : Message(Type::CompileResponse) {}

    template <typename T>
    void Serialize(T&& f) {
        f(error);
    }

    std::string error;
};

struct CompileRequest : Message {  // Client -> Server
    using Response = CompileResponse;

    CompileRequest() : Message(Type::CompileRequest) {}
    CompileRequest(SourceLanguage lang, int ver_major, int ver_minor, std::string src)
        : Message(Type::CompileRequest),
          language(lang),
          version_major(uint32_t(ver_major)),
          version_minor(uint32_t(ver_minor)),
          source(src) {}

    template <typename T>
    void Serialize(T&& f) {
        f(language);
        f(source);
        f(version_major);
        f(version_minor);
    }

    SourceLanguage language = SourceLanguage::MSL;
    uint32_t version_major = 0;
    uint32_t version_minor = 0;
    std::string source;
};

/// Writes the message `m` to the stream `s`
template <typename MESSAGE>
std::enable_if_t<std::is_base_of<Message, MESSAGE>::value, Stream>& operator<<(Stream& s,
                                                                               const MESSAGE& m) {
    s << m.type;
    const_cast<MESSAGE&>(m).Serialize([&s](const auto& value) { s << value; });
    return s;
}

/// Reads the message `m` from the stream `s`
template <typename MESSAGE>
std::enable_if_t<std::is_base_of<Message, MESSAGE>::value, Stream>& operator>>(Stream& s,
                                                                               MESSAGE& m) {
    Message::Type ty;
    s >> ty;
    if (s.error.empty()) {
        if (ty == m.type) {
            m.Serialize([&s](auto& value) { s >> value; });
        } else {
            std::stringstream ss;
            ss << "Expected message type " << static_cast<int>(m.type) << ", got "
               << static_cast<int>(ty);
            s.error = ss.str();
        }
    }
    return s;
}

/// Writes the request message `req` to the stream `s`, then reads and returns
/// the response message from the same stream.
template <typename REQUEST, typename RESPONSE = typename REQUEST::Response>
RESPONSE Send(Stream& s, const REQUEST& req) {
    s << req;
    if (s.error.empty()) {
        RESPONSE resp;
        s >> resp;
        if (s.error.empty()) {
            return resp;
        }
    }
    return {};
}

}  // namespace

bool RunServer(std::string port, bool verbose);
bool RunClient(std::string address,
               std::string port,
               std::string file,
               int version_major,
               int version_minor,
               bool verbose);

int main(int argc, char* argv[]) {
    bool run_server = false;
    bool verbose = false;
    int version_major = 0;
    int version_minor = 0;
    std::string port = "19000";

    std::regex metal_version_re{"^-?-std=macos-metal([0-9]+)\\.([0-9]+)"};

    std::vector<std::string> args;
    for (int i = 1; i < argc; i++) {
        std::string arg = argv[i];
        if (arg == "-s" || arg == "--server") {
            run_server = true;
            continue;
        }
        if (arg == "-p" || arg == "--port") {
            if (i < argc - 1) {
                i++;
                port = argv[i];
            } else {
                printf("expected port number");
                exit(1);
            }
            continue;
        }
        if (arg == "-v" || arg == "--verbose") {
            verbose = true;
            continue;
        }

        // xcrun flags are ignored so this executable can be used as a replacement for xcrun.
        if ((arg == "-x" || arg == "-sdk") && (i < argc - 1)) {
            i++;
            continue;
        }
        if (arg == "metal") {
            for (; i < argc; i++) {
                arg = argv[i];
                // metal_version_re
                std::smatch metal_version_match;
                if (std::regex_match(arg, metal_version_match, metal_version_re)) {
                    version_major = std::atoi(metal_version_match[1].str().c_str());
                    version_minor = std::atoi(metal_version_match[2].str().c_str());
                    continue;
                }
                if (arg == "-c") {
                    break;
                }
            }
            continue;
        }

        args.emplace_back(arg);
    }

    bool success = false;

    if (run_server) {
        success = RunServer(port, verbose);
    } else {
        std::string address;
        std::string file;
        switch (args.size()) {
            case 1:
                TINT_BEGIN_DISABLE_WARNING(DEPRECATED);
                if (auto* addr = getenv("TINT_REMOTE_COMPILE_ADDRESS")) {
                    address = addr;
                }
                TINT_END_DISABLE_WARNING(DEPRECATED);
                file = args[0];
                break;
            case 2:
                address = args[0];
                file = args[1];
                break;
            default:
                std::cerr << "Expected 1 or 2 arguments, got " << args.size() << "\n\n";
                ShowUsage();
        }
        if (address.empty() || file.empty()) {
            ShowUsage();
        }
        success = RunClient(address, port, file, version_major, version_minor, verbose);
    }

    if (!success) {
        exit(1);
    }

    return 0;
}

bool RunServer(std::string port, bool verbose) {
    auto server_socket = tint::socket::Socket::Listen("", port.c_str());
    if (!server_socket) {
        std::cout << "Failed to listen on port " << port << "\n";
        return false;
    }
    std::cout << "Listening on port " << port.c_str() << "...\n";
    while (auto conn = server_socket->Accept()) {
        std::thread([=] {
            auto tid = std::this_thread::get_id();
            if (verbose) {
                std::cout << tid << " Client connected...\n";
            }
            Stream stream{conn.get(), ""};

            {
                ConnectionRequest req;
                stream >> req;
                if (!stream.error.empty()) {
                    if (verbose) {
                        std::cout << tid << " Error: " << stream.error << "\n";
                    }
                    return;
                }
                ConnectionResponse resp;
                if (req.protocol_version != kProtocolVersion) {
                    if (verbose) {
                        std::cout << tid << " Protocol version mismatch. requested: "
                                  << req.protocol_version << "\n";
                    }
                    resp.error = "Protocol version mismatch";
                    stream << resp;
                    return;
                }
                stream << resp;
            }
            if (verbose) {
                std::cout << tid << " Connection established\n";
            }
            {
                CompileRequest req;
                stream >> req;
                if (!stream.error.empty()) {
                    if (verbose) {
                        std::cout << tid << " Error: " << stream.error << "\n";
                    }
                    return;
                }
#if TINT_BUILD_MSL_WRITER && defined(__APPLE__)
                if (req.language == SourceLanguage::MSL) {
                    auto version = tint::msl::validate::MslVersion::kMsl_1_2;
                    if (req.version_major == 2 && req.version_minor == 1) {
                        version = tint::msl::validate::MslVersion::kMsl_2_1;
                    }
                    if (req.version_major == 2 && req.version_minor == 3) {
                        version = tint::msl::validate::MslVersion::kMsl_2_3;
                    }
                    auto result = tint::msl::validate::ValidateUsingMetal(req.source, version);
                    CompileResponse resp;
                    if (result.failed) {
                        resp.error = result.output;
                    }
                    stream << resp;

                    if (verbose) {
                        std::cout << tid << " Shader compilation "
                                  << (result.failed ? "failed" : "passed") << "\n";
                    }
                    return;
                }
#endif
                CompileResponse resp;
                resp.error = "server cannot compile this type of shader";
                stream << resp;
            }
        }).detach();
    }
    return true;
}

bool RunClient(std::string address,
               std::string port,
               std::string file,
               int version_major,
               int version_minor,
               bool verbose) {
    // Read the file
    std::ifstream input(file, std::ios::binary);
    if (!input) {
        std::cerr << "Couldn't open '" << file << "'\n";
        return false;
    }
    std::string source((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());

    constexpr const int timeout_ms = 100'000;
    if (verbose) {
        std::cout << "Connecting to " << address << ":" << port << "\n";
    }
    auto conn = tint::socket::Socket::Connect(address.c_str(), port.c_str(), timeout_ms);
    if (!conn) {
        std::cerr << "Connection failed\n";
        return false;
    }

    Stream stream{conn.get(), ""};

    if (verbose) {
        std::cout << "Sending connection request...\n";
    }
    auto conn_resp = Send(stream, ConnectionRequest{kProtocolVersion});
    if (!stream.error.empty()) {
        std::cerr << stream.error << "\n";
        return false;
    }
    if (!conn_resp.error.empty()) {
        std::cerr << conn_resp.error << "\n";
        return false;
    }
    if (verbose) {
        std::cout << "Connection established. Requesting compile...\n";
    }
    auto comp_resp =
        Send(stream, CompileRequest{SourceLanguage::MSL, version_major, version_minor, source});
    if (!stream.error.empty()) {
        std::cerr << stream.error << "\n";
        return false;
    }
    if (!comp_resp.error.empty()) {
        std::cerr << comp_resp.error << "\n";
        return false;
    }
    if (verbose) {
        std::cout << "Compilation successful\n";
    }
    return true;
}
