// Copyright (c) 2018-2022 LG Electronics, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

#include "web_app_manager_service_agl.h"

#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>
#include <pthread.h>
#include <sys/file.h>
#include <sys/un.h>
#include <unistd.h>
#include <algorithm>
#include <cassert>
#include <climits>
#include <cstdlib>
#include <exception>
#include <fstream>
#include <iostream>
#include <set>
#include <sstream>

#include <json/value.h>

#include "log_manager.h"
#include "utils.h"
#include "wam_ipc.grpc.pb.h"
#include "web_app_base.h"
#include "web_app_manager.h"

namespace {
const char kDefaultGrpcServiceAddress[] = "127.0.0.1:15000";
}  // namespace

class WamIPCLockFile {
 public:
  WamIPCLockFile() {
    const char* runtime_dir;
    if ((runtime_dir = getenv("XDG_RUNTIME_DIR")) == NULL) {
      LOG_DEBUG("Failed to retrieve XDG_RUNTIME_DIR, falling back to /tmp");
      runtime_dir = "/tmp";
    }
    lock_file_ = std::string(runtime_dir);
    lock_file_.append("/wamipc.lock");
  }

  ~WamIPCLockFile() {
    if (lock_fd_ != -1)
      ReleaseLock(lock_fd_);
    if (lock_fd_ != -1)
      close(lock_fd_);
  }

  bool CreateAndLock() {
    lock_fd_ = OpenLockFile();
    if (!AcquireLock(lock_fd_)) {
      LOG_DEBUG("Failed to lock file %d", lock_fd_);
      return false;
    }
    return true;
  }

  bool OwnsLock() const { return lock_fd_ != -1; }

  bool TryAcquireLock() {
    int fd = OpenLockFile();
    if (fd != -1) {
      if (AcquireLock(fd)) {
        ReleaseLock(fd);
        return true;
      }
    }
    return false;
  }

 private:
  int OpenLockFile() {
    int fd = open(lock_file_.c_str(), O_CREAT | O_TRUNC, S_IRWXU);
    if (fd == -1) {
      LOG_DEBUG("Failed to open lock file descriptor");
      return fd;
    }

    int flags = fcntl(fd, F_GETFD);
    if (flags == -1)
      LOG_DEBUG("Could not get flags for lock file %d", fd);

    flags |= FD_CLOEXEC;

    if (fcntl(fd, F_SETFD, flags) == -1)
      LOG_DEBUG("Could not set flags for lock file %d", fd);

    return fd;
  }

  bool AcquireLock(int fd) {
    if (flock(fd, LOCK_EX | LOCK_NB) != 0)
      return false;
    return true;
  }

  void ReleaseLock(int fd) { flock(fd, LOCK_UN); }

  std::string lock_file_;
  int lock_fd_ = -1;
};

class GrpcServiceImpl final
    : public wam_ipc::WebAppManagerService::CallbackService {
  static AglShellSurfaceType SurfaceTypeProtoToWAM(
      wam_ipc::LaunchRequest_SurfaceType proto_surface_type) {
    switch (proto_surface_type) {
      case wam_ipc::LaunchRequest_SurfaceType_NONE:
        return AglShellSurfaceType::kNone;
      case wam_ipc::LaunchRequest_SurfaceType_BACKGROUND:
        return AglShellSurfaceType::kBackground;
      case wam_ipc::LaunchRequest_SurfaceType_PANEL:
        return AglShellSurfaceType::kPanel;
      default:
        return AglShellSurfaceType::kNone;
    }
  }
  static AglShellPanelEdge PanelEdgeProtoToWAM(
      wam_ipc::LaunchRequest_PanelEdge proto_panel_edge) {
    switch (proto_panel_edge) {
      case wam_ipc::LaunchRequest_PanelEdge_NOT_FOUND:
        return AglShellPanelEdge::kNotFound;
      case wam_ipc::LaunchRequest_PanelEdge_TOP:
        return AglShellPanelEdge::kTop;
      case wam_ipc::LaunchRequest_PanelEdge_BOTTOM:
        return AglShellPanelEdge::kBottom;
      case wam_ipc::LaunchRequest_PanelEdge_LEFT:
        return AglShellPanelEdge::kLeft;
      case wam_ipc::LaunchRequest_PanelEdge_RIGHT:
        return AglShellPanelEdge::kRight;
      default:
        return AglShellPanelEdge::kNotFound;
    }
  }
  grpc::ServerUnaryReactor* Launch(grpc::CallbackServerContext* context,
                                   const ::wam_ipc::LaunchRequest* request,
                                   google::protobuf::Empty* /*response*/) {
    WebAppManagerServiceAGL::LaunchParams launch_params;
    launch_params.app_id = request->app_id();
    launch_params.uri = request->uri();
    launch_params.surface_type = SurfaceTypeProtoToWAM(request->surface_type());
    launch_params.panel_edge = PanelEdgeProtoToWAM(request->panel_edge());
    launch_params.width = request->width();
    launch_params.height = request->height();

    WebAppManagerServiceAGL::Instance()->LaunchOnIdle(launch_params);

    grpc::ServerUnaryReactor* reactor = context->DefaultReactor();
    reactor->Finish(grpc::Status::OK);
    return reactor;
  }
  grpc::ServerUnaryReactor* Activate(grpc::CallbackServerContext* context,
                                     const ::wam_ipc::LaunchRequest* request,
                                     google::protobuf::Empty* /*response*/) {
    WebAppManagerServiceAGL::Instance()->SendEventOnIdle(kActivateEvent,
                                                         request->app_id());
    grpc::ServerUnaryReactor* reactor = context->DefaultReactor();
    reactor->Finish(grpc::Status::OK);
    return reactor;
  }
  grpc::ServerUnaryReactor* Kill(grpc::CallbackServerContext* context,
                                 const ::wam_ipc::LaunchRequest* request,
                                 google::protobuf::Empty* /*response*/) {
    WebAppManagerServiceAGL::Instance()->SendEventOnIdle(kKilledApp,
                                                         request->app_id());
    grpc::ServerUnaryReactor* reactor = context->DefaultReactor();
    reactor->Finish(grpc::Status::OK);
    return reactor;
  }
};

class GrpcClient {
 public:
  static wam_ipc::LaunchRequest_SurfaceType SurfaceTypeWAMToProto(
      AglShellSurfaceType wam_value) {
    switch (wam_value) {
      case AglShellSurfaceType::kNone:
        return wam_ipc::LaunchRequest_SurfaceType_NONE;
      case AglShellSurfaceType::kBackground:
        return wam_ipc::LaunchRequest_SurfaceType_BACKGROUND;
      case AglShellSurfaceType::kPanel:
        return wam_ipc::LaunchRequest_SurfaceType_PANEL;
      default:
        return wam_ipc::LaunchRequest_SurfaceType_NONE;
    }
  }
  static wam_ipc::LaunchRequest_PanelEdge PanelEdgeWAMToProto(
      AglShellPanelEdge wam_value) {
    switch (wam_value) {
      case AglShellPanelEdge::kNotFound:
        return wam_ipc::LaunchRequest_PanelEdge_NOT_FOUND;
      case AglShellPanelEdge::kTop:
        return wam_ipc::LaunchRequest_PanelEdge_TOP;
      case AglShellPanelEdge::kBottom:
        return wam_ipc::LaunchRequest_PanelEdge_BOTTOM;
      case AglShellPanelEdge::kLeft:
        return wam_ipc::LaunchRequest_PanelEdge_LEFT;
      case AglShellPanelEdge::kRight:
        return wam_ipc::LaunchRequest_PanelEdge_RIGHT;
      default:
        return wam_ipc::LaunchRequest_PanelEdge_NOT_FOUND;
    }
  }
  GrpcClient() {
    auto channel = grpc::CreateChannel(kDefaultGrpcServiceAddress,
                                       grpc::InsecureChannelCredentials());
    stub_ = wam_ipc::WebAppManagerService::NewStub(channel);
  }
  bool Launch(const WebAppManagerServiceAGL::LaunchParams& params) {
    wam_ipc::LaunchRequest request;
    request.set_app_id(params.app_id);
    request.set_uri(params.uri);
    request.set_surface_type(SurfaceTypeWAMToProto(params.surface_type));
    request.set_panel_edge(PanelEdgeWAMToProto(params.panel_edge));
    request.set_width(params.width);
    request.set_height(params.height);

    grpc::ClientContext context;
    google::protobuf::Empty reply;
    grpc::Status status = stub_->Launch(&context, request, &reply);
    return status.ok();
  }
  bool SendEvent(const std::string& event_name, const std::string& app_id) {
    grpc::ClientContext context;
    google::protobuf::Empty reply;
    if (event_name == kActivateEvent) {
      wam_ipc::ActivateRequest request;
      request.set_app_id(app_id);
      grpc::Status status = stub_->Activate(&context, request, &reply);
      return status.ok();
    } else if (event_name == kKilledApp) {
      wam_ipc::KillRequest request;
      request.set_app_id(app_id);
      grpc::Status status = stub_->Kill(&context, request, &reply);
      return status.ok();
    }
    return false;
  }

 private:
  std::unique_ptr<wam_ipc::WebAppManagerService::Stub> stub_;
};

WebAppManagerServiceAGL::WebAppManagerServiceAGL()
    : lock_file_(std::make_unique<WamIPCLockFile>()) {}

WebAppManagerServiceAGL* WebAppManagerServiceAGL::Instance() {
  static WebAppManagerServiceAGL* srv = new WebAppManagerServiceAGL();
  return srv;
}

bool WebAppManagerServiceAGL::InitializeAsHostService() {
  return lock_file_->CreateAndLock();
}

bool WebAppManagerServiceAGL::IsHostServiceRunning() {
  return !lock_file_->TryAcquireLock();
}

void WebAppManagerServiceAGL::LaunchOnHost(const LaunchParams& params) {
  LOG_DEBUG("Dispatching launchOnHost");
  if (!GetGrpcClient()->Launch(params))
    LOG_DEBUG("Failed to send Launch request");
}

void WebAppManagerServiceAGL::SendEvent(const std::string& event_name,
                                        const std::string& app_id) {
  LOG_DEBUG("Sending event");
  if (!GetGrpcClient()->SendEvent(event_name, app_id))
    LOG_DEBUG("Failed to send event");
}

void* RunGrpcService(void*) {
  std::string server_address(kDefaultGrpcServiceAddress);
  GrpcServiceImpl service;

  grpc::EnableDefaultHealthCheckService(true);
  grpc::reflection::InitProtoReflectionServerBuilderPlugin();

  grpc::ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);

  std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();

  return nullptr;
}

bool WebAppManagerServiceAGL::StartService() {
  if (lock_file_->OwnsLock()) {
    pthread_t thread_id;
    if (pthread_create(&thread_id, nullptr, RunGrpcService, nullptr) < 0) {
      perror("Could not create thread");
      LOG_DEBUG("Could not create thread...");
      return false;
    }
  }

  return true;
}

void WebAppManagerServiceAGL::LaunchOnIdle(const LaunchParams& params) {
  auto launch_params = std::make_unique<LaunchParams>(params);

  auto* timer =
      new OneShotTimerWithData<WebAppManagerServiceAGL, LaunchParams>();
  timer->Start(0, this, &WebAppManagerServiceAGL::OnLaunchApp,
               std::move(launch_params));
}

void WebAppManagerServiceAGL::SendEventOnIdle(const std::string& event,
                                              const std::string& app_id) {
  auto event_data = std::make_unique<EventData>();
  event_data->app_id = app_id;
  auto* timer = new OneShotTimerWithData<WebAppManagerServiceAGL, EventData>();
  if (event == kActivateEvent)
    timer->Start(0, this, &WebAppManagerServiceAGL::OnActivateEvent,
                 std::move(event_data));
  else if (event == kDeactivateEvent)
    timer->Start(0, this, &WebAppManagerServiceAGL::OnDeactivateEvent,
                 std::move(event_data));
  else if (event == kKilledApp)
    timer->Start(1000, this, &WebAppManagerServiceAGL::OnKillEvent,
                 std::move(event_data));
}

void WebAppManagerServiceAGL::OnLaunchApp(LaunchParams* params) {
  LOG_DEBUG("Triggering app start: %s", params->uri.c_str());
  if (!params->uri.empty()) {
    if (params->uri.find("http://") == 0) {
      LaunchStartupAppFromURL(params);
    } else {
      LaunchStartupAppFromJsonConfig(params);
    }
  }
}

void WebAppManagerServiceAGL::LaunchStartupAppFromJsonConfig(
    LaunchParams* params) {
  std::string configfile;
  configfile.append(params->uri);
  configfile.append("/appinfo.json");

  Json::Value root;
  Json::CharReaderBuilder builder;
  JSONCPP_STRING errs;

  std::ifstream ifs;
  ifs.open(configfile.c_str());

  if (!parseFromStream(builder, ifs, &root, &errs)) {
    LOG_DEBUG("Failed to parse %s configuration file", configfile.c_str());
  }

  root["folderPath"] = params->uri.c_str();

  if (params->width)
    root["widthOverride"] = params->width;
  if (params->height)
    root["heightOverride"] = params->height;

  root["surface_type"] = static_cast<int>(params->surface_type);
  root["panel_type"] = static_cast<int>(params->panel_edge);

  std::string app_desc = util::JsonToString(root);
  std::string empty_params = "{}";
  std::string app_id = root["id"].asString();
  int err_code = 0;
  std::string err_msg;
  WebAppManagerService::OnLaunch(app_desc, empty_params, app_id, err_code,
                                 err_msg);
}

void WebAppManagerServiceAGL::LaunchStartupAppFromURL(LaunchParams* params) {
  LOG_DEBUG("WebAppManagerServiceAGL::LaunchStartupAppFromURL");
  LOG_DEBUG("    url: %s", params->uri.c_str());
  Json::Value obj(Json::objectValue);
  obj["id"] = params->app_id;
  obj["version"] = "1.0";
  obj["vendor"] = "some vendor";
  obj["type"] = "web";
  obj["main"] = params->uri;
  obj["title"] = "webapp";
  obj["uiRevision"] = "2";
  obj["surface_type"] = static_cast<int>(params->surface_type);
  obj["panel_type"] = static_cast<int>(params->panel_edge);

  obj["widthOverride"] = params->width;
  obj["heightOverride"] = params->height;

  std::string app_desc = util::JsonToString(obj);
  std::string app_id = params->app_id;
  int err_code = 0;
  std::string empty_params = "{}";
  std::string err_msg;

  LOG_DEBUG("Launching with appDesc=[%s]", app_desc.c_str());

  WebAppManagerService::OnLaunch(app_desc, empty_params, app_id, err_code,
                                 err_msg);
  LOG_DEBUG("onLaunch: Done.");
}

Json::Value WebAppManagerServiceAGL::launchApp(const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::killApp(const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::pauseApp(const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::logControl(const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::setInspectorEnable(
    const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::closeAllApps(const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::discardCodeCache(
    const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::listRunningApps(const Json::Value& request,
                                                     bool subscribed) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::getWebProcessSize(
    const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::clearBrowsingData(
    const Json::Value& request) {
  return Json::Value(Json::objectValue);
}

Json::Value WebAppManagerServiceAGL::webProcessCreated(
    const Json::Value& request,
    bool subscribed) {
  return Json::Value(Json::objectValue);
}

void WebAppManagerServiceAGL::OnActivateEvent(EventData* event_data) {
  LOG_DEBUG("Activate app=%s", event_data->app_id.c_str());
  WebAppBase* web_app =
      WebAppManager::Instance()->FindAppById(event_data->app_id);
  if (web_app) {
    web_app->OnStageActivated();
    web_app->SendAglActivate(event_data->app_id.c_str());
  } else {
    LOG_DEBUG("Not found app=%s running", event_data->app_id.c_str());
  }
}

void WebAppManagerServiceAGL::OnDeactivateEvent(EventData* event_data) {
  LOG_DEBUG("Dectivate app=%s", event_data->app_id.c_str());
  WebAppBase* web_app =
      WebAppManager::Instance()->FindAppById(event_data->app_id);
  if (web_app)
    web_app->OnStageDeactivated();
}

void WebAppManagerServiceAGL::OnKillEvent(EventData* event_data) {
  LOG_DEBUG("Kill app=%s", event_data->app_id.c_str());
  WebAppManager::Instance()->OnKillApp(event_data->app_id, event_data->app_id);
}

GrpcClient* WebAppManagerServiceAGL::GetGrpcClient() {
  if (!grpc_client_) {
    grpc_client_ = std::make_unique<GrpcClient>();
  }
  return grpc_client_.get();
}
