
// orca-server by Nils Springob
// based on boost beast http server:
// Copyright (c) 2017 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
// Official repository: https://github.com/boostorg/beast
//

//------------------------------------------------------------------------------
//
// Example: HTTP server, small
//
//------------------------------------------------------------------------------

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio.hpp>
#include <chrono>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <memory>
#include <string>
#include <future>

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/foreach.hpp>
#ifdef _WIN32
#include <boost/dll/runtime_symbol_info.hpp>
#endif
#include <string>
#include <set>
#include <exception>
#include <iostream>

#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <boost/algorithm/string.hpp>

#include <boost/filesystem.hpp>

#include <boost/process.hpp>

#include <boost/lexical_cast.hpp>

#include <boost/algorithm/string/replace.hpp>
#include <boost/algorithm/string/predicate.hpp>

namespace fs = boost::filesystem;
namespace pt = boost::property_tree;
namespace bp = boost::process; 
namespace ip = boost::asio::ip;         // from <boost/asio.hpp>
using tcp = boost::asio::ip::tcp;       // from <boost/asio.hpp>
namespace http = boost::beast::http;    // from <boost/beast/http.hpp>


extern "C" void log_text(char * text);
void log_string(const std::string & text);
extern "C" void launchUserProgram(const char * cmd);
extern "C" bool ntservice_isservice();

#ifdef _WIN32
std::string readRegistryString(HKEY rootKey, const std::string & keyName, const std::string & valueName, const std::string & defaultValue);
#endif

void startBobDude(const fs::path & bobfile);


std::string decode64(const std::string &val) {
    using namespace boost::archive::iterators;
    using It = transform_width<binary_from_base64<std::string::const_iterator>, 8, 6>;
    return boost::algorithm::trim_right_copy_if(std::string(It(std::begin(val)), It(std::end(val))), [](char c) {
        return c == '\0';
    });
}


namespace my_program_state
{
    std::size_t
    request_count()
    {
        static std::size_t count = 0;
        count = (count>=9)? 0 : count+1;
        return count;
    }
    
    
    fs::path dataDir;
}

class http_connection : public std::enable_shared_from_this<http_connection>
{
public:
    http_connection(tcp::socket socket)
        : socket_(std::move(socket))
    {
    }

    // Initiate the asynchronous operations associated with the connection.
    void
    start()
    {
        read_request();
        check_deadline();
    }

private:
    // The socket for the currently connected client.
    tcp::socket socket_;

    // The buffer for performing reads.
    boost::beast::flat_buffer buffer_{8192};

    // The request message.
    http::request<http::dynamic_body> request_;

    // The response message.
    http::response<http::dynamic_body> response_;

    // The timer for putting a deadline on connection processing.
    boost::asio::basic_waitable_timer<std::chrono::steady_clock> deadline_{
#if BOOST_VERSION >= 107000
        socket_.get_executor(), std::chrono::seconds(60)
#else
        socket_.get_executor().context(), std::chrono::seconds(60)
#endif
    };

    // Asynchronously receive a complete request message.
    void
    read_request()
    {
        auto self = shared_from_this();

        http::async_read(
            socket_,
            buffer_,
            request_,
            [self](boost::beast::error_code ec,
                std::size_t bytes_transferred)
            {
                boost::ignore_unused(bytes_transferred);
                if(!ec)
                    self->process_request();
            });
    }

    // Determine what needs to be done with the request message.
    void
    process_request()
    {
        response_.version(request_.version());
        response_.keep_alive(false);
        response_.set(http::field::server, "BobDude-ORCA-Service");
        if (request_.count(http::field::origin)) {
            response_.set(http::field::access_control_allow_origin, request_[http::field::origin]);
            response_.set(http::field::vary, "Origin");
        }

        switch(request_.method())
        {
        case http::verb::post:
            response_.result(http::status::ok);
            create_post_response();
            break;
            
            
        case http::verb::get:
            response_.result(http::status::ok);
            create_get_response();
            break;

        default:
            response_.result(http::status::bad_request);
            response_.set(http::field::content_type, "text/plain");
            boost::beast::ostream(response_.body())
                << "Invalid request-method '"
                << request_.method_string().to_string()
                << "'";
            break;
        }

        write_response();
    }

    // Construct a response message based on the program state.
    void
    create_get_response()
    {
        if(request_.target() == "/listrobots")
        {
            //std::cout << "listrobots" << std::endl;
            response_.set(http::field::content_type, "text/plain; charset=utf-8");
            
            boost::beast::ostream(response_.body())
                << "[{\"Name\":\"/dev/ttyACM0\","
                << "\"SerialNumber\":\"\","
                << "\"DeviceClass\":\"\","
                << "\"Manufacturer\":\"\","
                << "\"Product\":\"\","
                << "\"IdProduct\":\"0x092e\","
                << "\"IdVendor\":\"0x16c0\","
                << "\"ISerial\":\"\","
                << "\"NetworkPort\":false}]\n";
        }
        else
        {
            response_.result(http::status::not_found);
            response_.set(http::field::content_type, "text/plain");
            boost::beast::ostream(response_.body()) << "File not found\r\n";
        }
    }    
    
    // Construct a response message based on the program state.
    void
    create_post_response()
    {
        if(request_.target() == "/upload")
        {
            log_string("upload");
          
            try {  
            //std::cout << request_ << std::endl;
            
            std::stringstream ss;
            ss << boost::beast::buffers_to_string(request_.body().data());
            pt::ptree json; 
            pt::read_json(ss, json); 
            
            std::string xhexfile = json.get<std::string>("xhex", "");
            std::string hexfile = json.get<std::string>("hex", "");
            
            if (hexfile!="") {
                hexfile = decode64(hexfile + "==");
            }
            
            std::string filename = "orcasrv-" + boost::lexical_cast<std::string>(my_program_state::request_count()) + ".bob3";
            fs::path filepath =  my_program_state::dataDir / filename;
            //log_string(filepath.string());
            //json.put<std::string>("hex","...");
            //pt::write_json(std::cout, json);
            
            {
                fs::ofstream f(filepath);
                if (xhexfile!="") {
                    // xhexfile will override hexfile!
                    f << xhexfile;
                } else {
                    // normalize hexfile
                    boost::replace_all(hexfile, "\r\n", "\n");
                    boost::replace_all(hexfile, "\n\r", "\n");
                    boost::replace_all(hexfile, "\r", "\n");
                    boost::replace_all(hexfile, "\n", "\r\n");
                    boost::to_upper(hexfile);
                    if (boost::starts_with(hexfile, "\r\n")) {
                        hexfile.erase(0, 2);
                    }
                    
                    
                    std::string platform = "bob3-m88-8";
                    std::string device_part = "atmega88";
                    std::string programmer = "avrispv2";
                                                     
                    if (boost::starts_with(hexfile, ":02000002BE330B\r\n:020000020000FC\r\n")) {
                        platform = "ROB3RTA";
                        device_part = "atmega328pb";
                        programmer = "avrispv2";
                        //std::cout << "iHEX-Tag: " << platform << std::endl;
                    } else if (boost::starts_with(hexfile, ":02000002B0B399\r\n:020000020000FC\r\n")) {
                        platform = "BOB3";
                        device_part = "atmega88";
                        programmer = "avrispv2";
                        //std::cout << "iHEX-Tag: " << platform << std::endl;
                    } else if (boost::starts_with(hexfile, ":02000002B9073C\r\n:020000020000FC\r\n")) {
                        platform = "BOB3-907-GOLD";
                        device_part = "efm32";
                        programmer = "progbob-armswd";
                        //std::cout << "iHEX-Tag: " << platform << std::endl;
                    }
                    
                    f   << "<?xml version=\"1.0\"?>\n"
                        << "<xhex version=\"1.0\">\n"
                        << "<platform>" << platform << "</platform>\n"
                        << "<programmer type=\"" << programmer << "\"/>\n"
                        << "<device part=\"" << device_part << "\" erase=\"yes\">\n"
                        << "<segment id=\"flash\" format=\"ihex\">\n"
                        << hexfile
                        << "</segment>\n"
                        << "</device>\n"
                        << "</xhex>";
                }
            }
            
            startBobDude(filepath);
            
          } catch (...) {
            std::cout << "Error during execution" << std::endl;
            log_string("Error during execution");
          }
            response_.set(http::field::content_type, "text/plain; charset=utf-8");
            boost::beast::ostream(response_.body())
                <<  "\n";
        }
        else
        {
            response_.result(http::status::not_found);
            response_.set(http::field::content_type, "text/plain");
            boost::beast::ostream(response_.body()) << "File not found\r\n";
        }
    }

    // Asynchronously transmit the response message.
    void
    write_response()
    {
        auto self = shared_from_this();

#if BOOST_VERSION >= 107400
        response_.content_length(response_.body().size());
#else
        response_.set(http::field::content_length, response_.body().size());
#endif

        http::async_write(
            socket_,
            response_,
            [self](boost::beast::error_code ec, std::size_t)
            {
                self->socket_.shutdown(tcp::socket::shutdown_send, ec);
                self->deadline_.cancel();
            });
    }

    // Check whether we have spent enough time on this connection.
    void
    check_deadline()
    {
        auto self = shared_from_this();

        deadline_.async_wait(
            [self](boost::beast::error_code ec)
            {
                if(!ec)
                {
                    // Close socket to cancel any outstanding operation.
                    self->socket_.close(ec);
                }
            });
    }
};

// "Loop" forever accepting new connections.
void
http_server(tcp::acceptor& acceptor, tcp::socket& socket)
{
  acceptor.async_accept(socket,
      [&](boost::beast::error_code ec)
      {
          if(!ec)
              std::make_shared<http_connection>(std::move(socket))->start();
          http_server(acceptor, socket);
      });
}


boost::asio::io_context * pioc;

#ifdef _WIN32
extern "C" int real_main(int argc, char* argv[]);
extern "C" void main_shutdown();


int real_main(int argc, char* argv[])
#else
int main(int argc, char* argv[])
#endif
{
    try
    {
        std::string tempDir = fs::temp_directory_path().make_preferred().string();
#ifdef _WIN32
        tempDir = readRegistryString(HKEY_LOCAL_MACHINE, "SOFTWARE\\BobDude\\orca", "dataDir", tempDir);
        log_string("DataDir: "+tempDir);
#endif  
        my_program_state::dataDir = tempDir;
        my_program_state::dataDir.make_preferred();
    
        auto const address = boost::asio::ip::make_address("0.0.0.0");
        unsigned short port = static_cast<unsigned short>(std::atoi("8991"));

        boost::asio::io_context ioc{1};
        pioc = &ioc;

        tcp::acceptor acceptor{ioc, {address, port}};
        tcp::socket socket{ioc};
        http_server(acceptor, socket);

        ioc.run();
    }
    catch(std::exception const& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }
}


void main_shutdown() {
    pioc->stop();

}

void startBobDude(const fs::path & _bobfile) {
    fs::path bobfile = _bobfile;

#ifdef _WIN32            
    fs::path progPath = boost::dll::program_location().remove_filename() / "bobdude.exe";
#else
    fs::path progPath = bp::search_path("bobdude");
#endif
	
  
#ifdef _WIN32            
    if (ntservice_isservice()) {
        std::string cmdline = progPath.make_preferred().string() + " --autoprog --autohide " + bobfile.make_preferred().string();
        log_string("cmd: "+cmdline);
        launchUserProgram(cmdline.c_str());
        return;
    }
#endif
    log_string("cmd: " + progPath.string() + " --autoprog --autohide " + bobfile.string());
    bp::spawn(progPath, "--autoprog", "--autohide", bobfile);
}

void log_text(char * text) {
   log_string(std::string(text));
}

void log_string(const std::string & text) {
#ifdef _DEBUG_TXT_
#ifdef _WIN32
    auto myfile = fopen("c:\\temp\\debug_orca.txt", "a");
    fprintf(myfile, text.c_str());
    fprintf(myfile, "\n\r");
    fclose (myfile);
#else
    std::cerr << text << std::endl;
#endif
#endif
}

#ifdef _WIN32

std::string readRegistryString(HKEY rootKey, const std::string & keyName, const std::string & valueName, const std::string & defaultValue) {
    HKEY key;
    
    long n = RegOpenKeyEx(rootKey, keyName.c_str(), 0, KEY_READ, &key);
    long n0=n;
    if (n!=ERROR_SUCCESS) n = RegOpenKeyEx(rootKey, keyName.c_str(), 0, KEY_READ|KEY_WOW64_64KEY, &key);
    if (n!=ERROR_SUCCESS) n = RegOpenKeyEx(rootKey, keyName.c_str(), 0, KEY_READ|KEY_WOW64_32KEY, &key);
    
    if (n != ERROR_SUCCESS) {
        log_string("Error RegOpenKey keyName='"+keyName+"' res="+boost::lexical_cast<std::string>(n0));
        
        return defaultValue;
    }
    DWORD value_length = 1024;
    DWORD value_type = REG_SZ;
    char value[1024];
    LSTATUS res = RegQueryValueEx(key, valueName.c_str(), NULL, &value_type, (LPBYTE)&value, &value_length);
    RegCloseKey(key);
    if (res==ERROR_SUCCESS) {
        if (value_length>0) {
            return std::string(value, value_length-1);
        } else {
            return "";
        }
    }
    log_string("Error RegQueryValueEx keyName='"+keyName+"' valueName='"+valueName+"'");
    return defaultValue;
}

#endif
