// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "text_delegate.h"

#include <fuchsia/ui/input/cpp/fidl.h>
#include <fuchsia/ui/input3/cpp/fidl.h>
#include <fuchsia/ui/views/cpp/fidl.h>
#include <lib/fidl/cpp/binding.h>

#include "flutter/fml/logging.h"
#include "flutter/fml/mapping.h"
#include "flutter/lib/ui/window/platform_message.h"
#include "flutter/shell/platform/fuchsia/flutter/keyboard.h"
#include "flutter/shell/platform/fuchsia/runtime/dart/utils/inlines.h"
#include "third_party/rapidjson/include/rapidjson/document.h"
#include "third_party/rapidjson/include/rapidjson/stringbuffer.h"
#include "third_party/rapidjson/include/rapidjson/writer.h"

#include "logging.h"

namespace flutter_runner {

static constexpr char kInputActionKey[] = "inputAction";

// See: https://api.flutter.dev/flutter/services/TextInputAction.html
// Only the actions relevant for Fuchsia are listed here.
static constexpr char kTextInputActionDone[] = "TextInputAction.done";
static constexpr char kTextInputActionNewline[] = "TextInputAction.newline";
static constexpr char kTextInputActionGo[] = "TextInputAction.go";
static constexpr char kTextInputActionNext[] = "TextInputAction.next";
static constexpr char kTextInputActionPrevious[] = "TextInputAction.previous";
static constexpr char kTextInputActionNone[] = "TextInputAction.none";
static constexpr char kTextInputActionSearch[] = "TextInputAction.search";
static constexpr char kTextInputActionSend[] = "TextInputAction.send";
static constexpr char kTextInputActionUnspecified[] =
    "TextInputAction.unspecified";

// Converts Flutter TextInputAction to Fuchsia action enum.
static fuchsia::ui::input::InputMethodAction IntoInputMethodAction(
    const std::string action_string) {
  if (action_string == kTextInputActionNewline) {
    return fuchsia::ui::input::InputMethodAction::NEWLINE;
  } else if (action_string == kTextInputActionDone) {
    return fuchsia::ui::input::InputMethodAction::DONE;
  } else if (action_string == kTextInputActionGo) {
    return fuchsia::ui::input::InputMethodAction::GO;
  } else if (action_string == kTextInputActionNext) {
    return fuchsia::ui::input::InputMethodAction::NEXT;
  } else if (action_string == kTextInputActionPrevious) {
    return fuchsia::ui::input::InputMethodAction::PREVIOUS;
  } else if (action_string == kTextInputActionNone) {
    return fuchsia::ui::input::InputMethodAction::NONE;
  } else if (action_string == kTextInputActionSearch) {
    return fuchsia::ui::input::InputMethodAction::SEARCH;
  } else if (action_string == kTextInputActionSend) {
    return fuchsia::ui::input::InputMethodAction::SEND;
  } else if (action_string == kTextInputActionUnspecified) {
    return fuchsia::ui::input::InputMethodAction::UNSPECIFIED;
  }
  // If this message comes along it means we should really add the missing 'if'
  // above.
  FML_VLOG(1) << "unexpected action_string: " << action_string;
  // Substituting DONE for an unexpected action string will probably be OK.
  return fuchsia::ui::input::InputMethodAction::DONE;
}

// Converts the Fuchsia action enum into Flutter TextInputAction.
static const std::string IntoTextInputAction(
    fuchsia::ui::input::InputMethodAction action) {
  if (action == fuchsia::ui::input::InputMethodAction::NEWLINE) {
    return kTextInputActionNewline;
  } else if (action == fuchsia::ui::input::InputMethodAction::DONE) {
    return kTextInputActionDone;
  } else if (action == fuchsia::ui::input::InputMethodAction::GO) {
    return kTextInputActionGo;
  } else if (action == fuchsia::ui::input::InputMethodAction::NEXT) {
    return kTextInputActionNext;
  } else if (action == fuchsia::ui::input::InputMethodAction::PREVIOUS) {
    return kTextInputActionPrevious;
  } else if (action == fuchsia::ui::input::InputMethodAction::NONE) {
    return kTextInputActionNone;
  } else if (action == fuchsia::ui::input::InputMethodAction::SEARCH) {
    return kTextInputActionSearch;
  } else if (action == fuchsia::ui::input::InputMethodAction::SEND) {
    return kTextInputActionSend;
  } else if (action == fuchsia::ui::input::InputMethodAction::UNSPECIFIED) {
    return kTextInputActionUnspecified;
  }
  // If this message comes along it means we should really add the missing 'if'
  // above.
  FML_VLOG(1) << "unexpected action: " << static_cast<uint32_t>(action);
  // Substituting "done" for an unexpected text input action will probably
  // be OK.
  return kTextInputActionDone;
}

// TODO(fxbug.dev/8868): Terminate engine if Fuchsia system FIDL connections
// have error.
template <class T>
void SetInterfaceErrorHandler(fidl::InterfacePtr<T>& interface,
                              std::string name) {
  interface.set_error_handler([name](zx_status_t status) {
    FML_LOG(ERROR) << "Interface error on: " << name << ", status: " << status;
  });
}
template <class T>
void SetInterfaceErrorHandler(fidl::Binding<T>& binding, std::string name) {
  binding.set_error_handler([name](zx_status_t status) {
    FML_LOG(ERROR) << "Binding error on: " << name << ", status: " << status;
  });
}

TextDelegate::TextDelegate(
    fuchsia::ui::views::ViewRef view_ref,
    fuchsia::ui::input::ImeServiceHandle ime_service,
    fuchsia::ui::input3::KeyboardHandle keyboard,
    std::function<void(std::unique_ptr<flutter::PlatformMessage>)>
        dispatch_callback)
    : dispatch_callback_(dispatch_callback),
      ime_client_(this),
      text_sync_service_(ime_service.Bind()),
      keyboard_listener_binding_(this),
      keyboard_(keyboard.Bind()) {
  // Register all error handlers.
  SetInterfaceErrorHandler(ime_, "Input Method Editor");
  SetInterfaceErrorHandler(ime_client_, "IME Client");
  SetInterfaceErrorHandler(text_sync_service_, "Text Sync Service");
  SetInterfaceErrorHandler(keyboard_listener_binding_, "Keyboard Listener");
  SetInterfaceErrorHandler(keyboard_, "Keyboard");

  // Configure keyboard listener.
  keyboard_->AddListener(std::move(view_ref),
                         keyboard_listener_binding_.NewBinding(), [] {});
}

void TextDelegate::ActivateIme() {
  ActivateIme(requested_text_action_.value_or(
      fuchsia::ui::input::InputMethodAction::DONE));
}

void TextDelegate::ActivateIme(fuchsia::ui::input::InputMethodAction action) {
  DEBUG_CHECK(last_text_state_.has_value(), LOG_TAG, "");

  requested_text_action_ = action;
  text_sync_service_->GetInputMethodEditor(
      fuchsia::ui::input::KeyboardType::TEXT,  // keyboard type
      action,                                  // input method action
      last_text_state_.value(),                // initial state
      ime_client_.NewBinding(),                // client
      ime_.NewRequest()                        // editor
  );
}

void TextDelegate::DeactivateIme() {
  if (ime_) {
    text_sync_service_->HideKeyboard();
    ime_ = nullptr;
  }
  if (ime_client_.is_bound()) {
    ime_client_.Unbind();
  }
}

// |fuchsia::ui::input::InputMethodEditorClient|
void TextDelegate::DidUpdateState(
    fuchsia::ui::input::TextInputState state,
    std::unique_ptr<fuchsia::ui::input::InputEvent> input_event) {
  rapidjson::Document document;
  auto& allocator = document.GetAllocator();
  rapidjson::Value encoded_state(rapidjson::kObjectType);
  encoded_state.AddMember("text", state.text, allocator);
  encoded_state.AddMember("selectionBase", state.selection.base, allocator);
  encoded_state.AddMember("selectionExtent", state.selection.extent, allocator);
  switch (state.selection.affinity) {
    case fuchsia::ui::input::TextAffinity::UPSTREAM:
      encoded_state.AddMember("selectionAffinity",
                              rapidjson::Value("TextAffinity.upstream"),
                              allocator);
      break;
    case fuchsia::ui::input::TextAffinity::DOWNSTREAM:
      encoded_state.AddMember("selectionAffinity",
                              rapidjson::Value("TextAffinity.downstream"),
                              allocator);
      break;
  }
  encoded_state.AddMember("selectionIsDirectional", true, allocator);
  encoded_state.AddMember("composingBase", state.composing.start, allocator);
  encoded_state.AddMember("composingExtent", state.composing.end, allocator);

  rapidjson::Value args(rapidjson::kArrayType);
  args.PushBack(current_text_input_client_, allocator);
  args.PushBack(encoded_state, allocator);

  document.SetObject();
  document.AddMember("method",
                     rapidjson::Value("TextInputClient.updateEditingState"),
                     allocator);
  document.AddMember("args", args, allocator);

  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);

  const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
  dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
      kTextInputChannel,                                 // channel
      fml::MallocMapping::Copy(data, buffer.GetSize()),  // message
      nullptr)                                           // response
  );
  last_text_state_ = std::move(state);
}

// |fuchsia::ui::input::InputMethodEditorClient|
void TextDelegate::OnAction(fuchsia::ui::input::InputMethodAction action) {
  rapidjson::Document document;
  auto& allocator = document.GetAllocator();

  rapidjson::Value args(rapidjson::kArrayType);
  args.PushBack(current_text_input_client_, allocator);

  const std::string action_string = IntoTextInputAction(action);
  args.PushBack(rapidjson::Value{}.SetString(action_string.c_str(),
                                             action_string.length()),
                allocator);

  document.SetObject();
  document.AddMember(
      "method", rapidjson::Value("TextInputClient.performAction"), allocator);
  document.AddMember("args", args, allocator);

  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);

  const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
  dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
      kTextInputChannel,                                 // channel
      fml::MallocMapping::Copy(data, buffer.GetSize()),  // message
      nullptr)                                           // response
  );
}

// Channel handler for kTextInputChannel
bool TextDelegate::HandleFlutterTextInputChannelPlatformMessage(
    std::unique_ptr<flutter::PlatformMessage> message) {
  FML_DCHECK(message->channel() == kTextInputChannel);
  const auto& data = message->data();

  rapidjson::Document document;
  document.Parse(reinterpret_cast<const char*>(data.GetMapping()),
                 data.GetSize());
  if (document.HasParseError() || !document.IsObject()) {
    return false;
  }
  auto root = document.GetObject();
  auto method = root.FindMember("method");
  if (method == root.MemberEnd() || !method->value.IsString()) {
    return false;
  }

  if (method->value == "TextInput.show") {
    if (ime_) {
      text_sync_service_->ShowKeyboard();
    }
  } else if (method->value == "TextInput.hide") {
    if (ime_) {
      text_sync_service_->HideKeyboard();
    }
  } else if (method->value == "TextInput.setClient") {
    // Sample "setClient" message:
    //
    // {
    //   "method": "TextInput.setClient",
    //   "args": [
    //      7,
    //      {
    //        "inputType": {
    //          "name": "TextInputType.multiline",
    //          "signed":null,
    //          "decimal":null
    //        },
    //        "readOnly": false,
    //        "obscureText": false,
    //        "autocorrect":true,
    //        "smartDashesType":"1",
    //        "smartQuotesType":"1",
    //        "enableSuggestions":true,
    //        "enableInteractiveSelection":true,
    //        "actionLabel":null,
    //        "inputAction":"TextInputAction.newline",
    //        "textCapitalization":"TextCapitalization.none",
    //        "keyboardAppearance":"Brightness.dark",
    //        "enableIMEPersonalizedLearning":true,
    //        "enableDeltaModel":false
    //     }
    //  ]
    // }

    current_text_input_client_ = 0;
    DeactivateIme();
    auto args = root.FindMember("args");
    if (args == root.MemberEnd() || !args->value.IsArray() ||
        args->value.Size() != 2)
      return false;
    const auto& configuration = args->value[1];
    if (!configuration.IsObject()) {
      return false;
    }
    // TODO(abarth): Read the keyboard type from the configuration.
    current_text_input_client_ = args->value[0].GetInt();

    auto initial_text_input_state = fuchsia::ui::input::TextInputState{};
    initial_text_input_state.text = "";
    last_text_state_ = std::move(initial_text_input_state);

    const auto configuration_object = configuration.GetObject();
    if (!configuration_object.HasMember(kInputActionKey)) {
      return false;
    }
    const auto& action_object = configuration_object[kInputActionKey];
    if (!action_object.IsString()) {
      return false;
    }
    const auto action_string =
        std::string(action_object.GetString(), action_object.GetStringLength());
    ActivateIme(IntoInputMethodAction(std::move(action_string)));
  } else if (method->value == "TextInput.setEditingState") {
    if (ime_) {
      auto args_it = root.FindMember("args");
      if (args_it == root.MemberEnd() || !args_it->value.IsObject()) {
        return false;
      }
      const auto& args = args_it->value;
      fuchsia::ui::input::TextInputState state;
      state.text = "";
      // TODO(abarth): Deserialize state.
      auto text = args.FindMember("text");
      if (text != args.MemberEnd() && text->value.IsString()) {
        state.text = text->value.GetString();
      }
      auto selection_base = args.FindMember("selectionBase");
      if (selection_base != args.MemberEnd() && selection_base->value.IsInt()) {
        state.selection.base = selection_base->value.GetInt();
      }
      auto selection_extent = args.FindMember("selectionExtent");
      if (selection_extent != args.MemberEnd() &&
          selection_extent->value.IsInt()) {
        state.selection.extent = selection_extent->value.GetInt();
      }
      auto selection_affinity = args.FindMember("selectionAffinity");
      if (selection_affinity != args.MemberEnd() &&
          selection_affinity->value.IsString() &&
          selection_affinity->value == "TextAffinity.upstream") {
        state.selection.affinity = fuchsia::ui::input::TextAffinity::UPSTREAM;
      } else {
        state.selection.affinity = fuchsia::ui::input::TextAffinity::DOWNSTREAM;
      }
      // We ignore selectionIsDirectional because that concept doesn't exist on
      // Fuchsia.
      auto composing_base = args.FindMember("composingBase");
      if (composing_base != args.MemberEnd() && composing_base->value.IsInt()) {
        state.composing.start = composing_base->value.GetInt();
      }
      auto composing_extent = args.FindMember("composingExtent");
      if (composing_extent != args.MemberEnd() &&
          composing_extent->value.IsInt()) {
        state.composing.end = composing_extent->value.GetInt();
      }
      ime_->SetState(std::move(state));
    }
  } else if (method->value == "TextInput.clearClient") {
    current_text_input_client_ = 0;
    last_text_state_ = std::nullopt;
    requested_text_action_ = std::nullopt;
    DeactivateIme();
  } else if (method->value == "TextInput.setCaretRect" ||
             method->value == "TextInput.setEditableSizeAndTransform" ||
             method->value == "TextInput.setMarkedTextRect" ||
             method->value == "TextInput.setStyle") {
    // We don't have these methods implemented and they get
    // sent a lot during text input, so we create an empty case for them
    // here to avoid "Unknown flutter/textinput method TextInput.*"
    // log spam.
    //
    // TODO(fxb/101619): We should implement these.
  } else {
    FML_LOG(ERROR) << "Unknown " << message->channel() << " method "
                   << method->value.GetString();
  }
  // Complete with an empty response.
  return false;
}

// |fuchsia::ui:input3::KeyboardListener|
void TextDelegate::OnKeyEvent(
    fuchsia::ui::input3::KeyEvent key_event,
    fuchsia::ui::input3::KeyboardListener::OnKeyEventCallback callback) {
  const char* type = nullptr;
  switch (key_event.type()) {
    case fuchsia::ui::input3::KeyEventType::PRESSED:
      type = "keydown";
      break;
    case fuchsia::ui::input3::KeyEventType::RELEASED:
      type = "keyup";
      break;
    case fuchsia::ui::input3::KeyEventType::SYNC:
      // SYNC means the key was pressed while focus was not on this application.
      // This should possibly behave like PRESSED in the future, though it
      // doesn't hurt to ignore it today.
    case fuchsia::ui::input3::KeyEventType::CANCEL:
      // CANCEL means the key was released while focus was not on this
      // application.
      // This should possibly behave like RELEASED in the future to ensure that
      // a key is not repeated forever if it is pressed while focus is lost.
    default:
      break;
  }
  if (type == nullptr) {
    FML_VLOG(1) << "Unknown key event phase.";
    callback(fuchsia::ui::input3::KeyEventStatus::NOT_HANDLED);
    return;
  }
  keyboard_translator_.ConsumeEvent(std::move(key_event));

  rapidjson::Document document;
  auto& allocator = document.GetAllocator();
  document.SetObject();
  document.AddMember("type", rapidjson::Value(type, strlen(type)), allocator);
  document.AddMember("keymap", rapidjson::Value("fuchsia"), allocator);
  document.AddMember("hidUsage", keyboard_translator_.LastHIDUsage(),
                     allocator);
  document.AddMember("codePoint", keyboard_translator_.LastCodePoint(),
                     allocator);
  document.AddMember("modifiers", keyboard_translator_.Modifiers(), allocator);
  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  document.Accept(writer);

  const uint8_t* data = reinterpret_cast<const uint8_t*>(buffer.GetString());
  dispatch_callback_(std::make_unique<flutter::PlatformMessage>(
      kKeyEventChannel,                                  // channel
      fml::MallocMapping::Copy(data, buffer.GetSize()),  // data
      nullptr)                                           // response
  );
  callback(fuchsia::ui::input3::KeyEventStatus::HANDLED);
}
}  // namespace flutter_runner
