/*
 * 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_renderer.h"

#include <drm/drm_fourcc.h>

#include "host/libs/config/cuttlefish_config.h"

namespace cuttlefish {
namespace confui {
static teeui::Color alfaCombineChannel(std::uint32_t shift, double alfa,
                                       teeui::Color a, teeui::Color b) {
  a >>= shift;
  a &= 0xff;
  b >>= shift;
  b &= 0xff;
  double acc = alfa * a + (1 - alfa) * b;
  if (acc <= 0) {
    return 0;
  }
  std::uint32_t result = acc;
  if (result > 255) {
    return 255 << shift;
  }
  return result << shift;
}

/**
 * create a raw frame for confirmation UI dialog
 *
 * Many rendering code borrowed from the following source
 *  https://android.googlesource.com/trusty/app/confirmationui/+/0429cc7/src
 */
class ConfUiRendererImpl {
  friend class ConfUiRenderer;

 public:
  using LabelConfMsg = teeui::LabelBody;

 private:
  static Result<std::unique_ptr<ConfUiRendererImpl>> GenerateRenderer(
      const std::uint32_t display, const std::string& confirmation_msg,
      const std::string& locale, const bool inverted, const bool magnified);

  /**
   * this does not repaint from the scratch all the time
   *
   * It does repaint its frame buffer only when w/h of
   * current display has changed
   */
  std::unique_ptr<TeeUiFrameWrapper>& RenderRawFrame();

  bool IsFrameReady() const { return raw_frame_ && !raw_frame_->IsEmpty(); }

  bool IsInConfirm(const std::uint32_t x, const std::uint32_t y) {
    return IsInside<teeui::LabelOK>(x, y);
  }
  bool IsInCancel(const std::uint32_t x, const std::uint32_t y) {
    return IsInside<teeui::LabelCancel>(x, y);
  }

  bool IsSetUpSuccessful() const { return is_setup_well_; }
  ConfUiRendererImpl(const std::uint32_t display,
                     const std::string& confirmation_msg,
                     const std::string& locale, const bool inverted,
                     const bool magnified);

  struct Boundary {            // inclusive but.. LayoutElement's size is float
    std::uint32_t x, y, w, h;  // (x, y) is the top left
  };

  template <typename LayoutElement>
  Boundary GetBoundary(LayoutElement&& e) const {
    auto box = e.bounds_;
    Boundary b;
    // (x,y) is left top. so floor() makes sense
    // w, h are width and height in float. perhaps ceiling makes more
    // sense
    b.x = static_cast<std::uint32_t>(box.x().floor().count());
    b.y = static_cast<std::uint32_t>(box.y().floor().count());
    b.w = static_cast<std::uint32_t>(box.w().ceil().count());
    b.h = static_cast<std::uint32_t>(box.h().ceil().count());
    return b;
  }

  template <typename Element>
  bool IsInside(const std::uint32_t x, const std::uint32_t y) const {
    auto box = GetBoundary(std::get<Element>(layout_));
    if (x >= box.x && x <= box.x + box.w && y >= box.y && y <= box.y + box.h) {
      return true;
    }
    return false;
  }
  // essentially, to repaint from the scratch, so returns new frame
  // when successful. Or, nullopt
  std::unique_ptr<TeeUiFrameWrapper> RepaintRawFrame(const int w, const int h);

  bool InitLayout(const std::string& lang_id);
  teeui::Error UpdateTranslations();
  teeui::Error UpdateLocale();
  void SetDeviceContext(const unsigned long long w, const unsigned long long h,
                        bool is_inverted, bool is_magnified);

  // a callback function to be effectively sent to TeeUI library
  teeui::Error UpdatePixels(TeeUiFrameWrapper& buffer, std::uint32_t x,
                            std::uint32_t y, teeui::Color color);

  // second param is for type deduction
  template <typename... Elements>
  static teeui::Error drawElements(std::tuple<Elements...>& layout,
                                   const teeui::PixelDrawer& drawPixel) {
    // Error::operator|| is overloaded, so we don't get short circuit
    // evaluation. But we get the first error that occurs. We will still try and
    // draw the remaining elements in the order they appear in the layout tuple.
    return (std::get<Elements>(layout).draw(drawPixel) || ...);
  }
  void UpdateColorScheme(const bool is_inverted);
  template <typename Label>
  auto SetText(const std::string& text) {
    return std::get<Label>(layout_).setText(
        {text.c_str(), text.c_str() + text.size()});
  }

  template <typename Label>
  teeui::Error UpdateString();

  std::uint32_t display_num_;
  teeui::layout_t<teeui::ConfUILayout> layout_;
  std::string lang_id_;
  std::string prompt_text_;  // confirmation ui message

  /**
   * Potentially, the same frame could be requested multiple times.
   *
   * While another thread/caller is using this frame, the frame should
   * be kept here, too, to be returned upon future requests.
   *
   */
  std::unique_ptr<TeeUiFrameWrapper> raw_frame_;
  std::uint32_t current_height_;
  std::uint32_t current_width_;
  teeui::Color color_bg_;
  teeui::Color color_text_;
  teeui::Color shield_color_;
  bool is_inverted_;
  bool is_magnified_;
  teeui::context<teeui::ConfUIParameters> ctx_;
  bool is_setup_well_;

  static constexpr const teeui::Color kColorBackground = 0xffffffff;
  static constexpr const teeui::Color kColorBackgroundInv = 0xff212121;
  static constexpr const teeui::Color kColorDisabled = 0xffbdbdbd;
  static constexpr const teeui::Color kColorDisabledInv = 0xff424242;
  static constexpr const teeui::Color kColorEnabled = 0xff212121;
  static constexpr const teeui::Color kColorEnabledInv = 0xffdedede;
  static constexpr const teeui::Color kColorShield = 0xff778500;
  static constexpr const teeui::Color kColorShieldInv = 0xffc4cb80;
  static constexpr const teeui::Color kColorText = 0xff212121;
  static constexpr const teeui::Color kColorTextInv = 0xffdedede;
};

Result<std::unique_ptr<ConfUiRendererImpl>>
ConfUiRendererImpl::GenerateRenderer(const std::uint32_t display,
                                     const std::string& confirmation_msg,
                                     const std::string& locale,
                                     const bool inverted,
                                     const bool magnified) {
  ConfUiRendererImpl* raw_ptr = new ConfUiRendererImpl(
      display, confirmation_msg, locale, inverted, magnified);
  CF_EXPECT(raw_ptr && raw_ptr->IsSetUpSuccessful(),
            "Failed to create ConfUiRendererImpl");
  return std::unique_ptr<ConfUiRendererImpl>(raw_ptr);
}

static int GetDpi(const int display_num = 0) {
  auto config = CuttlefishConfig::Get();
  CHECK(config) << "Config is Missing";
  auto instance = config->ForDefaultInstance();
  auto display_configs = instance.display_configs();
  CHECK_GT(display_configs.size(), display_num)
      << "Invalid display number " << display_num;
  return display_configs[display_num].dpi;
}

/**
 * device configuration
 *
 * ctx_{# of pixels in 1 mm, # of pixels per 1 density independent pixels}
 *
 * The numbers are, however, to fit for the host webRTC local/remote clients
 * in general, not necessarily the supposedly guest device (e.g. Auto, phone,
 * etc)
 *
 * In general, for a normal PC, roughly ctx_(6.45211, 400.0/412.0) is a good
 * combination for the default DPI, 320. If we want to see the impact
 * of the change in the guest DPI, we could adjust the combination above
 * proportionally
 *
 */
ConfUiRendererImpl::ConfUiRendererImpl(const std::uint32_t display,
                                       const std::string& confirmation_msg,
                                       const std::string& locale,
                                       const bool inverted,
                                       const bool magnified)
    : display_num_{display},
      lang_id_{locale},
      prompt_text_{confirmation_msg},
      current_height_{ScreenConnectorInfo::ScreenHeight(display_num_)},
      current_width_{ScreenConnectorInfo::ScreenWidth(display_num_)},
      is_inverted_(inverted),
      is_magnified_(magnified),
      ctx_(6.45211 * GetDpi() / 320.0, 400.0 / 412.0 * GetDpi() / 320.0),
      is_setup_well_(false) {
  SetDeviceContext(current_width_, current_height_, is_inverted_,
                   is_magnified_);
  layout_ = teeui::instantiateLayout(teeui::ConfUILayout(), ctx_);

  if (auto error = UpdateLocale()) {
    ConfUiLog(ERROR) << "Update Translation Error: " << Enum2Base(error.code());
    // is_setup_well_ = false;
    return;
  }
  UpdateColorScheme(is_inverted_);
  SetText<LabelConfMsg>(prompt_text_);
  is_setup_well_ = true;
}

teeui::Error ConfUiRendererImpl::UpdateLocale() {
  using teeui::Error;
  teeui::localization::selectLangId(lang_id_.c_str());
  if (auto error = UpdateTranslations()) {
    return error;
  }
  return Error::OK;
}

template <typename Label>
teeui::Error ConfUiRendererImpl::UpdateString() {
  using namespace teeui;
  const char* str;
  auto& label = std::get<Label>(layout_);
  str = localization::lookup(TranslationId(label.textId()));
  if (str == nullptr) {
    ConfUiLog(ERROR) << "Given translation_id" << label.textId() << "not found";
    return Error::Localization;
  }
  label.setText({str, str + strlen(str)});
  return Error::OK;
}

teeui::Error ConfUiRendererImpl::UpdateTranslations() {
  using namespace teeui;
  if (auto error = UpdateString<LabelOK>()) {
    return error;
  }
  if (auto error = UpdateString<LabelCancel>()) {
    return error;
  }
  if (auto error = UpdateString<LabelTitle>()) {
    return error;
  }
  if (auto error = UpdateString<LabelHint>()) {
    return error;
  }
  return Error::OK;
}

void ConfUiRendererImpl::SetDeviceContext(const unsigned long long w,
                                          const unsigned long long h,
                                          const bool is_inverted,
                                          const bool is_magnified) {
  using namespace teeui;
  const auto screen_width = operator""_px(w);
  const auto screen_height = operator""_px(h);
  ctx_.setParam<RightEdgeOfScreen>(pxs(screen_width));
  ctx_.setParam<BottomOfScreen>(pxs(screen_height));
  if (is_magnified) {
    ctx_.setParam<DefaultFontSize>(18_dp);
    ctx_.setParam<BodyFontSize>(20_dp);
  } else {
    ctx_.setParam<DefaultFontSize>(14_dp);
    ctx_.setParam<BodyFontSize>(16_dp);
  }
  if (is_inverted) {
    ctx_.setParam<ShieldColor>(kColorShieldInv);
    ctx_.setParam<ColorText>(kColorTextInv);
    ctx_.setParam<ColorBG>(kColorBackgroundInv);
    ctx_.setParam<ColorButton>(kColorShieldInv);
  } else {
    ctx_.setParam<ShieldColor>(kColorShield);
    ctx_.setParam<ColorText>(kColorText);
    ctx_.setParam<ColorBG>(kColorBackground);
    ctx_.setParam<ColorButton>(kColorShield);
  }
}

teeui::Error ConfUiRendererImpl::UpdatePixels(TeeUiFrameWrapper& raw_frame,
                                              std::uint32_t x, std::uint32_t y,
                                              teeui::Color color) {
  auto buffer = raw_frame.data();
  const auto height = raw_frame.Height();
  const auto width = raw_frame.Width();
  auto pos = width * y + x;
  if (pos >= (height * width)) {
    ConfUiLog(ERROR) << "Rendering Out of Bound";
    return teeui::Error::OutOfBoundsDrawing;
  }
  const double alfa = ((color & 0xff000000) >> 24) / 255.0;
  auto& pixel = *reinterpret_cast<teeui::Color*>(buffer + pos);
  pixel = alfaCombineChannel(0, alfa, color, pixel) |
          alfaCombineChannel(8, alfa, color, pixel) |
          alfaCombineChannel(16, alfa, color, pixel);
  return teeui::Error::OK;
}

void ConfUiRendererImpl::UpdateColorScheme(const bool is_inverted) {
  using namespace teeui;
  color_text_ = is_inverted ? kColorDisabledInv : kColorDisabled;
  shield_color_ = is_inverted ? kColorShieldInv : kColorShield;
  color_bg_ = is_inverted ? kColorBackgroundInv : kColorBackground;

  ctx_.setParam<ShieldColor>(shield_color_);
  ctx_.setParam<ColorText>(color_text_);
  ctx_.setParam<ColorBG>(color_bg_);
  return;
}

std::unique_ptr<TeeUiFrameWrapper>& ConfUiRendererImpl::RenderRawFrame() {
  /* we repaint only if one or more of the following meet:
   *
   *  1. raw_frame_ is empty
   *  2. the current_width_ and current_height_ is out of date
   *
   */
  const int w = ScreenConnectorInfo::ScreenWidth(display_num_);
  const int h = ScreenConnectorInfo::ScreenHeight(display_num_);
  if (!IsFrameReady() || current_height_ != h || current_width_ != w) {
    auto new_frame = RepaintRawFrame(w, h);
    if (!new_frame) {
      // must repaint but failed
      raw_frame_ = nullptr;
      return raw_frame_;
    }
    // repainting from the scratch successful in a new frame
    raw_frame_ = std::move(new_frame);
    current_width_ = w;
    current_height_ = h;
  }
  return raw_frame_;
}

std::unique_ptr<TeeUiFrameWrapper> ConfUiRendererImpl::RepaintRawFrame(
    const int w, const int h) {
  std::get<teeui::LabelOK>(layout_).setTextColor(kColorEnabled);
  std::get<teeui::LabelCancel>(layout_).setTextColor(kColorEnabled);

  /**
   * should be uint32_t for teeui APIs.
   * It assumes that each raw frame buffer element is 4 bytes
   */
  const teeui::Color background_color =
      is_inverted_ ? kColorBackgroundInv : kColorBackground;
  auto new_raw_frame =
      std::make_unique<TeeUiFrameWrapper>(w, h, background_color);
  auto draw_pixel = teeui::makePixelDrawer(
      [this, &new_raw_frame](std::uint32_t x, std::uint32_t y,
                             teeui::Color color) -> teeui::Error {
        return this->UpdatePixels(*new_raw_frame, x, y, color);
      });

  // render all components
  const auto error = drawElements(layout_, draw_pixel);
  if (error) {
    ConfUiLog(ERROR) << "Painting failed: " << error.code();
    return nullptr;
  }

  return new_raw_frame;
}

ConfUiRenderer::ConfUiRenderer(ScreenConnectorFrameRenderer& screen_connector)
    : screen_connector_{screen_connector} {}

ConfUiRenderer::~ConfUiRenderer() {}

Result<void> ConfUiRenderer::RenderDialog(
    const std::uint32_t display_num, const std::string& prompt_text,
    const std::string& locale, const std::vector<teeui::UIOption>& ui_options) {
  renderer_impl_ = CF_EXPECT(ConfUiRendererImpl::GenerateRenderer(
      display_num, prompt_text, locale, IsInverted(ui_options),
      IsMagnified(ui_options)));
  auto& teeui_frame = renderer_impl_->RenderRawFrame();
  CF_EXPECT(teeui_frame != nullptr, "RenderRawFrame() failed.");
  ConfUiLog(VERBOSE) << "actually trying to render the frame"
                     << thread::GetName();
  auto frame_width = teeui_frame->Width();
  auto frame_height = teeui_frame->Height();
  auto frame_stride_bytes = teeui_frame->ScreenStrideBytes();
  auto frame_bytes = reinterpret_cast<std::uint8_t*>(teeui_frame->data());
  CF_EXPECT(screen_connector_.RenderConfirmationUi(
      display_num, frame_width, frame_height, DRM_FORMAT_ABGR8888,
      frame_stride_bytes, frame_bytes));
  return {};
}

bool ConfUiRenderer::IsInverted(
    const std::vector<teeui::UIOption>& ui_options) const {
  return Contains(ui_options, teeui::UIOption::AccessibilityInverted);
}

bool ConfUiRenderer::IsMagnified(
    const std::vector<teeui::UIOption>& ui_options) const {
  return Contains(ui_options, teeui::UIOption::AccessibilityMagnified);
}

bool ConfUiRenderer::IsInConfirm(const std::uint32_t x, const std::uint32_t y) {
  if (!renderer_impl_) {
    ConfUiLog(INFO) << "renderer_impl_ is nullptr";
  }
  return renderer_impl_ && renderer_impl_->IsInConfirm(x, y);
}
bool ConfUiRenderer::IsInCancel(const std::uint32_t x, const std::uint32_t y) {
  if (!renderer_impl_) {
    ConfUiLog(INFO) << "renderer_impl_ is nullptr";
  }
  return renderer_impl_ && renderer_impl_->IsInCancel(x, y);
}

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