/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * 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.
 */

#include "host/libs/confui/host_server.h"

#include <functional>
#include <memory>
#include <optional>
#include <tuple>

#include "common/libs/confui/confui.h"
#include "common/libs/fs/shared_buf.h"
#include "host/libs/config/cuttlefish_config.h"
#include "host/libs/confui/host_utils.h"
#include "host/libs/confui/secure_input.h"

namespace cuttlefish {
namespace confui {
namespace {

template <typename Derived, typename Base>
std::unique_ptr<Derived> DowncastTo(std::unique_ptr<Base>&& base) {
  Base* tmp = base.release();
  Derived* derived = static_cast<Derived*>(tmp);
  return std::unique_ptr<Derived>(derived);
}

}  // namespace

/**
 * null if not user/touch, or wrap it and ConfUiSecure{Selection,Touch}Message
 *
 * ConfUiMessage must NOT ConfUiSecure{Selection,Touch}Message types
 */
static std::unique_ptr<ConfUiMessage> WrapWithSecureFlag(
    std::unique_ptr<ConfUiMessage>&& base_msg, const bool secure) {
  switch (base_msg->GetType()) {
    case ConfUiCmd::kUserInputEvent: {
      auto as_selection =
          DowncastTo<ConfUiUserSelectionMessage>(std::move(base_msg));
      return ToSecureSelectionMessage(std::move(as_selection), secure);
    }
    case ConfUiCmd::kUserTouchEvent: {
      auto as_touch = DowncastTo<ConfUiUserTouchMessage>(std::move(base_msg));
      return ToSecureTouchMessage(std::move(as_touch), secure);
    }
    default:
      return nullptr;
  }
}

HostServer::HostServer(HostModeCtrl& host_mode_ctrl,
                       ConfUiRenderer& host_renderer,
                       const PipeConnectionPair& fd_pair)
    : display_num_(0),
      host_renderer_{host_renderer},
      host_mode_ctrl_(host_mode_ctrl),
      from_guest_fifo_fd_(fd_pair.from_guest_),
      to_guest_fifo_fd_(fd_pair.to_guest_) {
  const size_t max_elements = 20;
  auto ignore_new =
      [](ThreadSafeQueue<std::unique_ptr<ConfUiMessage>>::QueueImpl*) {
        // no op, so the queue is still full, and the new item will be discarded
        return;
      };
  hal_cmd_q_id_ = input_multiplexer_.RegisterQueue(
      HostServer::Multiplexer::CreateQueue(max_elements, ignore_new));
  user_input_evt_q_id_ = input_multiplexer_.RegisterQueue(
      HostServer::Multiplexer::CreateQueue(max_elements, ignore_new));
}

bool HostServer::IsVirtioConsoleOpen() const {
  return from_guest_fifo_fd_->IsOpen() && to_guest_fifo_fd_->IsOpen();
}

bool HostServer::CheckVirtioConsole() {
  if (IsVirtioConsoleOpen()) {
    return true;
  }
  ConfUiLog(FATAL) << "Virtio console is not open";
  return false;
}

void HostServer::Start() {
  if (!CheckVirtioConsole()) {
    return;
  }
  auto hal_cmd_fetching = [this]() { this->HalCmdFetcherLoop(); };
  auto main = [this]() { this->MainLoop(); };
  hal_input_fetcher_thread_ =
      thread::RunThread("HalInputLoop", hal_cmd_fetching);
  main_loop_thread_ = thread::RunThread("MainLoop", main);
  ConfUiLog(DEBUG) << "host service started.";
  return;
}

void HostServer::HalCmdFetcherLoop() {
  while (true) {
    if (!CheckVirtioConsole()) {
      return;
    }
    auto msg = RecvConfUiMsg(from_guest_fifo_fd_);
    if (!msg) {
      ConfUiLog(ERROR) << "Error in RecvConfUiMsg from HAL";
      // TODO(kwstephenkim): error handling
      // either file is not open, or ill-formatted message
      continue;
    }
    /*
     * In case of Vts test, the msg could be a user input. For now, we do not
     * enforce the input grace period for Vts. However, if ever we do, here is
     * where the time point check should happen. Once it is enqueued, it is not
     * always guaranteed to be picked up reasonably soon.
     */
    constexpr bool is_secure = false;
    auto to_override_if_user_input =
        WrapWithSecureFlag(std::move(msg), is_secure);
    if (to_override_if_user_input) {
      msg = std::move(to_override_if_user_input);
    }
    input_multiplexer_.Push(hal_cmd_q_id_, std::move(msg));
  }
}

/**
 * Send inputs generated not by auto-tester but by the human users
 *
 * Send such inputs into the command queue consumed by the state machine
 * in the main loop/current session.
 */
void HostServer::SendUserSelection(std::unique_ptr<ConfUiMessage>& input) {
  if (!curr_session_ ||
      curr_session_->GetState() != MainLoopState::kInSession ||
      !curr_session_->IsReadyForUserInput()) {
    // ignore
    return;
  }
  constexpr bool is_secure = true;
  auto secure_input = WrapWithSecureFlag(std::move(input), is_secure);
  input_multiplexer_.Push(user_input_evt_q_id_, std::move(secure_input));
}

void HostServer::TouchEvent(const int x, const int y, const bool is_down) {
  if (!is_down || !curr_session_) {
    return;
  }
  std::unique_ptr<ConfUiMessage> input =
      std::make_unique<ConfUiUserTouchMessage>(GetCurrentSessionId(), x, y);
  SendUserSelection(input);
}

void HostServer::UserAbortEvent() {
  if (!curr_session_) {
    return;
  }
  std::unique_ptr<ConfUiMessage> input =
      std::make_unique<ConfUiUserSelectionMessage>(GetCurrentSessionId(),
                                                   UserResponse::kUserAbort);
  SendUserSelection(input);
}

// read the comments in the header file
[[noreturn]] void HostServer::MainLoop() {
  while (true) {
    // this gets one input from either queue:
    // from HAL or from all webrtc clients
    // if no input, sleep until there is
    auto input_ptr = input_multiplexer_.Pop();
    auto& input = *input_ptr;
    const auto session_id = input.GetSessionId();
    const auto cmd = input.GetType();
    const std::string cmd_str(ToString(cmd));

    // take input for the Finite States Machine below
    std::string src = input.IsUserInput() ? "input" : "hal";
    ConfUiLog(VERBOSE) << "In Session " << GetCurrentSessionId() << ", "
                       << "in state " << GetCurrentState() << ", "
                       << "received input from " << src << " cmd =" << cmd_str
                       << " going to session " << session_id;

    if (!curr_session_) {
      if (cmd != ConfUiCmd::kStart) {
        ConfUiLog(VERBOSE) << ToString(cmd) << " to " << session_id
                           << " is ignored as there is no session to receive";
        continue;
      }
      // the session is created as kInit
      curr_session_ = CreateSession(input.GetSessionId());
    }
    if (cmd == ConfUiCmd::kUserTouchEvent) {
      ConfUiSecureUserTouchMessage& touch_event =
          static_cast<ConfUiSecureUserTouchMessage&>(input);
      auto [x, y] = touch_event.GetLocation();
      const bool is_confirm = curr_session_->IsConfirm(x, y);
      const bool is_cancel = curr_session_->IsCancel(x, y);
      ConfUiLog(INFO) << "Touch at [" << x << ", " << y << "] was "
                      << (is_cancel ? "CANCEL"
                                    : (is_confirm ? "CONFIRM" : "INVALID"));
      if (!is_confirm && !is_cancel) {
        // ignore, take the next input
        continue;
      }
      decltype(input_ptr) tmp_input_ptr =
          std::make_unique<ConfUiUserSelectionMessage>(
              GetCurrentSessionId(),
              (is_confirm ? UserResponse::kConfirm : UserResponse::kCancel));
      input_ptr =
          WrapWithSecureFlag(std::move(tmp_input_ptr), touch_event.IsSecure());
    }
    Transition(input_ptr);

    // finalize
    if (curr_session_ &&
        curr_session_->GetState() == MainLoopState::kAwaitCleanup) {
      curr_session_->CleanUp();
      curr_session_ = nullptr;
    }
  }  // end of the infinite while loop
}

std::shared_ptr<Session> HostServer::CreateSession(const std::string& name) {
  return std::make_shared<Session>(name, display_num_, host_renderer_,
                                   host_mode_ctrl_);
}

static bool IsUserAbort(ConfUiMessage& msg) {
  if (msg.GetType() != ConfUiCmd::kUserInputEvent) {
    return false;
  }
  ConfUiUserSelectionMessage& selection =
      static_cast<ConfUiUserSelectionMessage&>(msg);
  return (selection.GetResponse() == UserResponse::kUserAbort);
}

void HostServer::Transition(std::unique_ptr<ConfUiMessage>& input_ptr) {
  auto& input = *input_ptr;
  const auto session_id = input.GetSessionId();
  const auto cmd = input.GetType();
  const std::string cmd_str(ToString(cmd));
  FsmInput fsm_input = ToFsmInput(input);
  ConfUiLog(VERBOSE) << "Handling " << ToString(cmd);
  if (IsUserAbort(input)) {
    curr_session_->UserAbort(to_guest_fifo_fd_);
    return;
  }

  if (cmd == ConfUiCmd::kAbort) {
    curr_session_->Abort();
    return;
  }
  curr_session_->Transition(to_guest_fifo_fd_, fsm_input, input);
}

}  // end of namespace confui
}  // end of namespace cuttlefish
