// Copyright 2010-2021, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "win32/tip/tip_ui_element_delegate.h"

#define _ATL_NO_AUTOMATIC_NAMESPACE
#define _WTL_NO_AUTOMATIC_NAMESPACE
#include <atlbase.h>
#include <atlcom.h>
#include <atlstr.h>
#include <msctf.h>

#include <string>

#include "base/logging.h"
#include "base/util.h"
#include "protocol/commands.pb.h"
#include "protocol/renderer_command.pb.h"
#include "win32/base/conversion_mode_util.h"
#include "win32/base/input_state.h"
#include "win32/tip/tip_dll_module.h"
#include "win32/tip/tip_edit_session.h"
#include "win32/tip/tip_private_context.h"
#include "win32/tip/tip_resource.h"
#include "win32/tip/tip_text_service.h"

namespace mozc {
namespace win32 {
namespace tsf {

namespace {

using ATL::CComBSTR;
using ATL::CComPtr;
using ATL::CComQIPtr;
using ATL::CComVariant;
using ATL::CStringW;
using ::mozc::commands::CandidateList;
using mozc::commands::CompositionMode;
using ::mozc::commands::Output;
using ::mozc::commands::Status;
typedef mozc::commands::RendererCommand_IndicatorInfo IndicatorInfo;

const size_t kPageSize = 9;

// This GUID is used in Windows Vista/7/8 by MS-IME to represents if the
// candidate window is visible or not.
// TODO(yukawa): Make sure if it is safe to use this GUID.
// {B7A578D2-9332-438A-A403-4057D05C3958}
const GUID kGuidCUASCandidateMessageCompartment = {
    0xb7a578d2,
    0x9332,
    0x438a,
    {0xa4, 0x03, 0x40, 0x57, 0xd0, 0x5c, 0x39, 0x58}};

#ifdef GOOGLE_JAPANESE_INPUT_BUILD

// {8F51B5E5-5CF9-45D8-83B3-53CE203354C2}
const GUID KGuidNonobservableSuggestWindow = {
    0x8f51b5e5,
    0x5cf9,
    0x45d8,
    {0x83, 0xb3, 0x53, 0xce, 0x20, 0x33, 0x54, 0xc2}};

// {3D53878A-8596-4689-B50D-3338D52B2EFB}
const GUID KGuidObservableSuggestWindow = {
    0x3d53878a,
    0x8596,
    0x4689,
    {0xb5, 0xd, 0x33, 0x38, 0xd5, 0x2b, 0x2e, 0xfb}};

// {FED897F2-940C-40F1-B149-A931E03FB821}
const GUID KGuidCandidateWindow = {
    0xfed897f2,
    0x940c,
    0x40f1,
    {0xb1, 0x49, 0xa9, 0x31, 0xe0, 0x3f, 0xb8, 0x21}};

// {170F6CC4-913D-4FF9-9DEA-432D08DCB0FF}
const GUID KGuidIndicatorWindow = {
    0x170f6cc4,
    0x913d,
    0x4ff9,
    {0x9d, 0xea, 0x43, 0x2d, 0x8, 0xdc, 0xb0, 0xff}};

#else

// {AD2489FB-D4C4-4632-85A9-7F9F917AB0FD}
const GUID KGuidNonobservableSuggestWindow = {
    0xad2489fb,
    0xd4c4,
    0x4632,
    {0x85, 0xa9, 0x7f, 0x9f, 0x91, 0x7a, 0xb0, 0xfd}};

// {0E2D447F-9B4A-490C-9C4D-61A6A707BE26}
const GUID KGuidObservableSuggestWindow = {
    0xe2d447f, 0x9b4a, 0x490c, {0x9c, 0x4d, 0x61, 0xa6, 0xa7, 0x7, 0xbe, 0x26}};

// {ED70ECDE-C8AA-4170-96CC-0090DEA8AEC2}
const GUID KGuidCandidateWindow = {
    0xed70ecde,
    0xc8aa,
    0x4170,
    {0x96, 0xcc, 0x0, 0x90, 0xde, 0xa8, 0xae, 0xc2}};

// {0090BF80-5F33-41B1-843C-E3EC79ED25F9}
const GUID KGuidIndicatorWindow = {
    0x90bf80, 0x5f33, 0x41b1, {0x84, 0x3c, 0xe3, 0xec, 0x79, 0xed, 0x25, 0xf9}};

#endif  // GOOGLE_JAPANESE_INPUT_BUILD

CComBSTR GetResourceString(UINT resource_id) {
  CStringW str;
  str.LoadStringW(TipDllModule::module_handle(), resource_id);
  return CComBSTR(str.GetLength(), str.GetBuffer());
}

const bool kIsIndicator = true;
const bool kIsNotIndicator = false;

class TipUiElementDelegateImpl : public TipUiElementDelegate {
 public:
  TipUiElementDelegateImpl(TipTextService *text_service, ITfContext *context,
                           TipUiElementDelegateFactory::ElementType type)
      : text_service_(text_service), context_(context), type_(type) {}

 private:
  ~TipUiElementDelegateImpl() {}

  virtual bool IsObservable() const {
    switch (type_) {
      case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow:
      case TipUiElementDelegateFactory::kConventionalCandidateWindow:
        return true;
      default:
        return false;
    }
  }

  // The ITfUIElement interface methods
  virtual HRESULT GetDescription(BSTR *description) {
    if (description == nullptr) {
      return E_INVALIDARG;
    }
    *description = nullptr;
    switch (type_) {
      case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow:
        *description =
            GetResourceString(IDS_UNOBSERVABLE_SUGGEST_WINDOW).Detach();
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow:
        *description =
            GetResourceString(IDS_OBSERVABLE_SUGGEST_WINDOW).Detach();
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalCandidateWindow:
        *description = GetResourceString(IDS_CANDIDATE_WINDOW).Detach();
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalIndicatorWindow:
        *description = GetResourceString(IDS_INDICATOR_WINDOW).Detach();
        return S_OK;
      case TipUiElementDelegateFactory::kImmersiveCandidateWindow:
        *description = GetResourceString(IDS_CANDIDATE_WINDOW).Detach();
        return S_OK;
      case TipUiElementDelegateFactory::kImmersiveIndicatorWindow:
        *description = GetResourceString(IDS_INDICATOR_WINDOW).Detach();
        return S_OK;
      default:
        return E_UNEXPECTED;
    }
  }

  virtual HRESULT GetGUID(GUID *guid) {
    if (guid == nullptr) {
      return E_INVALIDARG;
    }
    switch (type_) {
      case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow:
        *guid = KGuidNonobservableSuggestWindow;
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow:
        *guid = KGuidObservableSuggestWindow;
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalCandidateWindow:
        *guid = KGuidCandidateWindow;
        return S_OK;
      case TipUiElementDelegateFactory::kConventionalIndicatorWindow:
        *guid = KGuidIndicatorWindow;
        return S_OK;
      case TipUiElementDelegateFactory::kImmersiveCandidateWindow:
        *guid = KGuidCandidateWindow;
        return S_OK;
      case TipUiElementDelegateFactory::kImmersiveIndicatorWindow:
        *guid = KGuidIndicatorWindow;
        return S_OK;
      default:
        *guid = GUID_NULL;
        return E_UNEXPECTED;
    }
  }

  virtual HRESULT Show(BOOL show) {
    const bool old_shown = shown_;
    shown_ = !!show;
    if (old_shown != shown_ && IsObservable()) {
      CComQIPtr<ITfCompartmentMgr> compartment_mgr = context_;
      if (compartment_mgr) {
        // Update a hidden compartment to generate
        // IMN_OPENCANDIDATE/IMN_CLOSECANDIDATE notifications for the
        // application compatibility.
        CComQIPtr<ITfCompartment> compartment;
        if (SUCCEEDED(compartment_mgr->GetCompartment(
                kGuidCUASCandidateMessageCompartment, &compartment))) {
          CComVariant var;
          var.vt = VT_I4;
          var.lVal = shown_ ? TRUE : FALSE;
          compartment->SetValue(text_service_->GetClientID(), &var);
        }
      }
      // TODO(yukawa): Update UI.
    }
    return S_OK;
  }

  virtual HRESULT IsShown(BOOL *show) {
    if (show == nullptr) {
      return E_INVALIDARG;
    }
    *show = (shown_ ? TRUE : FALSE);
    return S_OK;
  }

  // The ITfCandidateListUIElement interface methods
  virtual HRESULT GetUpdatedFlags(DWORD *flags) {
    DCHECK(IsCandidateWindowLike());

    if (flags == nullptr) {
      return E_INVALIDARG;
    }
    *flags = 0;
    // If TF_CLUIE_STRING is included into |flags|, TSF calls back
    // ITfCandidateListUIElement::GetString for all the candidates,
    // which might be a huge bottleneck. So do not include this flag
    // whenever possible.
    if (TestModifiedAndUpdateLastCandidate()) {
      *flags |= (TF_CLUIE_STRING | TF_CLUIE_COUNT);
    }
    *flags |= (TF_CLUIE_SELECTION | TF_CLUIE_CURRENTPAGE | TF_CLUIE_PAGEINDEX);
    return S_OK;
  }

  virtual HRESULT GetDocumentMgr(ITfDocumentMgr **document_manager) {
    DCHECK(IsCandidateWindowLike());

    if (document_manager == nullptr) {
      return E_INVALIDARG;
    }
    return context_->GetDocumentMgr(document_manager);
  }

  virtual HRESULT GetCount(UINT *count) {
    DCHECK(IsCandidateWindowLike());

    if (count == nullptr) {
      return E_INVALIDARG;
    }
    *count = 0;
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return S_OK;
    }
    *count = output.all_candidate_words().candidates_size();
    return S_OK;
  }

  virtual HRESULT GetSelection(UINT *index) {
    DCHECK(IsCandidateWindowLike());

    if (index == nullptr) {
      return E_INVALIDARG;
    }
    *index = 0;
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return S_OK;
    }
    *index = output.all_candidate_words().focused_index();
    return S_OK;
  }

  virtual HRESULT GetString(UINT index, BSTR *text) {
    DCHECK(IsCandidateWindowLike());

    if (text == nullptr) {
      return E_INVALIDARG;
    }
    *text = nullptr;
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return E_FAIL;
    }
    const CandidateList &list = output.all_candidate_words();
    // Convert |index| to the index within output_->candidates().
    const int visible_index = index;
    if (visible_index >= list.candidates_size()) {
      return E_FAIL;
    }
    std::wstring wide_text;
    Util::UTF8ToWide(list.candidates(visible_index).value(), &wide_text);
    *text = CComBSTR(wide_text.size(), wide_text.data()).Detach();
    return S_OK;
  }

  virtual HRESULT GetPageIndex(UINT *index, UINT size, UINT *page_count) {
    DCHECK(IsCandidateWindowLike());

    if (page_count == nullptr) {
      return E_INVALIDARG;
    }
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return E_FAIL;
    }
    const CandidateList &list = output.all_candidate_words();
    const size_t max_page = (list.candidates_size() / kPageSize);
    *page_count = max_page + 1;

    if (index == nullptr) {
      // An application can pass nullptr as |index| to obtain only page_count.
      return S_OK;
    }

    if (size < *page_count) {
      return E_NOT_SUFFICIENT_BUFFER;
    }
    for (size_t i = 0; i < *page_count; ++i) {
      index[i] = i * kPageSize;
    }
    return S_OK;
  }

  virtual HRESULT SetPageIndex(UINT *index, UINT page_count) {
    DCHECK(IsCandidateWindowLike());

    return E_NOTIMPL;
  }

  virtual HRESULT GetCurrentPage(UINT *current_page) {
    DCHECK(IsCandidateWindowLike());

    if (current_page == nullptr) {
      return E_INVALIDARG;
    }
    *current_page = 0;
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return S_OK;
    }
    *current_page = output.all_candidate_words().focused_index() / kPageSize;
    return S_OK;
  }

  // The ITfCandidateListUIElementBehavior interface methods
  virtual HRESULT SetSelection(UINT index) {
    DCHECK(IsCandidateWindowLike());

    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return E_FAIL;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return E_FAIL;
    }
    const CandidateList &list = output.all_candidate_words();
    if (list.candidates_size() <= index) {
      return E_INVALIDARG;
    }
    const int id = list.candidates(index).id();
    if (!TipEditSession::SelectCandidateAsync(text_service_, context_, id)) {
      return E_FAIL;
    }
    return S_OK;
  }

  virtual HRESULT Finalize() {
    DCHECK(IsCandidateWindowLike());

    if (!TipEditSession::SubmitAsync(text_service_, context_)) {
      return E_FAIL;
    }
    return S_OK;
  }

  virtual HRESULT Abort() {
    DCHECK(IsCandidateWindowLike());

    // Currently equals to Finalize().
    if (!TipEditSession::SubmitAsync(text_service_, context_)) {
      return E_FAIL;
    }
    return S_OK;
  }

  virtual HRESULT GetString(BSTR *text) {
    DCHECK(IsIndicator());

    if (text == nullptr) {
      return E_INVALIDARG;
    }

    CStringW msg = L"";

    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach();
      return S_OK;
    }
    if (!private_context->last_output().has_status()) {
      *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach();
      return S_OK;
    }
    const Status &status = private_context->last_output().status();
    if (status.has_activated() && !status.activated()) {
      msg = L"A";
      *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach();
      return S_OK;
    }
    if (!status.has_mode()) {
      *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach();
      return S_OK;
    }

    switch (status.mode()) {
      case commands::DIRECT:
        DLOG(FATAL) << "Must not reach here.";
        break;
      case commands::HIRAGANA:
        msg = L"\u3042";
        break;
      case commands::FULL_KATAKANA:
        msg = L"\u30AB";
        break;
      case commands::HALF_ASCII:
        msg = L"_A";
        break;
      case commands::FULL_ASCII:
        msg = L"\uFF21";
        break;
      case commands::HALF_KATAKANA:
        msg = L"_\uFF76";
        break;
      default:
        break;
    }

    *text = CComBSTR(msg.GetLength(), msg.GetBuffer()).Detach();
    return S_OK;
  }

  // Returns true if the candidate list is updated. When this function returns
  // false, you need not update the list of candidate strings at this time.
  // Note that this function updates |last_candidate_list_| internally.
  bool TestModifiedAndUpdateLastCandidate() {
    TipPrivateContext *private_context =
        text_service_->GetPrivateContext(context_);
    if (private_context == nullptr) {
      return true;
    }
    const Output &output = private_context->last_output();
    if (!output.has_all_candidate_words()) {
      return true;
    }
    const CandidateList &list = output.all_candidate_words();
    if (last_candidate_list_.candidates_size() != list.candidates_size()) {
      last_candidate_list_.CopyFrom(list);
      return true;
    }
    for (int i = 0; i < list.candidates_size(); ++i) {
      if (last_candidate_list_.candidates(i).value() !=
          list.candidates(i).value()) {
        last_candidate_list_.CopyFrom(list);
        return true;
      }
    }
    return false;
  }

  bool IsCandidateWindowLike() const {
    switch (type_) {
      case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow:
        return true;
      case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow:
        return true;
      case TipUiElementDelegateFactory::kConventionalCandidateWindow:
        return true;
      case TipUiElementDelegateFactory::kConventionalIndicatorWindow:
        return false;
      case TipUiElementDelegateFactory::kImmersiveCandidateWindow:
        return true;
      case TipUiElementDelegateFactory::kImmersiveIndicatorWindow:
        return false;
      default:
        return false;
    }
  }

  bool IsIndicator() const {
    switch (type_) {
      case TipUiElementDelegateFactory::kConventionalUnobservableSuggestWindow:
        return false;
      case TipUiElementDelegateFactory::kConventionalObservableSuggestWindow:
        return false;
      case TipUiElementDelegateFactory::kConventionalCandidateWindow:
        return false;
      case TipUiElementDelegateFactory::kConventionalIndicatorWindow:
        return true;
      case TipUiElementDelegateFactory::kImmersiveCandidateWindow:
        return false;
      case TipUiElementDelegateFactory::kImmersiveIndicatorWindow:
        return true;
      default:
        return false;
    }
  }

  CComPtr<TipTextService> text_service_;
  CComPtr<ITfContext> context_;
  const TipUiElementDelegateFactory::ElementType type_;
  CandidateList last_candidate_list_;
  bool shown_;
  DISALLOW_COPY_AND_ASSIGN(TipUiElementDelegateImpl);
};

}  // namespace

TipUiElementDelegate::TipUiElementDelegate() {}
TipUiElementDelegate::~TipUiElementDelegate() {}

TipUiElementDelegate *TipUiElementDelegateFactory::Create(
    TipTextService *text_service, ITfContext *context, ElementType type) {
  return new TipUiElementDelegateImpl(text_service, context, type);
}

}  // namespace tsf
}  // namespace win32
}  // namespace mozc
