// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "components/open_from_clipboard/clipboard_recent_content_ios.h"

#import <CommonCrypto/CommonDigest.h>
#import <UIKit/UIKit.h>
#import <stddef.h>
#import <stdint.h>

#import "base/metrics/user_metrics.h"
#import "base/notimplemented.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "base/system/sys_info.h"
#import "base/task/sequenced_task_runner.h"
#import "components/open_from_clipboard/clipboard_recent_content_impl_ios.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"
#import "url/url_constants.h"

namespace {

// Schemes accepted by the ClipboardRecentContentIOS.
constexpr std::array<const char*, 4> kAuthorizedSchemes = {
    url::kHttpScheme,
    url::kHttpsScheme,
    url::kDataScheme,
    url::kAboutScheme,
};

// Get the list of authorized schemes.
NSSet<NSString*>* GetAuthorizedSchemeList(std::string_view application_scheme) {
  NSMutableSet<NSString*>* schemes = [NSMutableSet set];
  for (const char* scheme : kAuthorizedSchemes) {
    [schemes addObject:base::SysUTF8ToNSString(scheme)];
  }
  if (!application_scheme.empty()) {
    [schemes addObject:base::SysUTF8ToNSString(application_scheme)];
  }

  return [schemes copy];
}

ContentType ContentTypeFromClipboardContentType(ClipboardContentType type) {
  switch (type) {
    case ClipboardContentType::URL:
      return ContentTypeURL;
    case ClipboardContentType::Text:
      return ContentTypeText;
    case ClipboardContentType::Image:
      return ContentTypeImage;
  }
}

ClipboardContentType ClipboardContentTypeFromContentType(ContentType type) {
  if ([type isEqualToString:ContentTypeURL]) {
    return ClipboardContentType::URL;
  } else if ([type isEqualToString:ContentTypeText]) {
    return ClipboardContentType::Text;
  } else if ([type isEqualToString:ContentTypeImage]) {
    return ClipboardContentType::Image;
  }
  NOTREACHED();
}

}  // namespace

@interface ClipboardRecentContentDelegateImpl
    : NSObject<ClipboardRecentContentDelegate>
@end

@implementation ClipboardRecentContentDelegateImpl

- (void)onClipboardChanged {
  base::RecordAction(base::UserMetricsAction("MobileOmniboxClipboardChanged"));
}

@end

ClipboardRecentContentIOS::ClipboardRecentContentIOS(
    std::string_view application_scheme,
    NSUserDefaults* group_user_defaults)
    : ClipboardRecentContentIOS([[ClipboardRecentContentImplIOS alloc]
             initWithMaxAge:MaximumAgeOfClipboard().InSecondsF()
          authorizedSchemes:GetAuthorizedSchemeList(application_scheme)
               userDefaults:group_user_defaults
                   delegate:[[ClipboardRecentContentDelegateImpl alloc]
                                init]]) {}

ClipboardRecentContentIOS::ClipboardRecentContentIOS(
    ClipboardRecentContentImplIOS* implementation) {
  implementation_ = implementation;
}

void ClipboardRecentContentIOS::HasRecentContentFromClipboard(
    std::set<ClipboardContentType> types,
    HasDataCallback callback) {
  __block HasDataCallback callback_for_block = std::move(callback);
  NSMutableSet<ContentType>* ios_types = [NSMutableSet set];
  for (ClipboardContentType type : types) {
    [ios_types addObject:ContentTypeFromClipboardContentType(type)];
  }
  // The iOS methods for checking clipboard content call their callbacks on an
  // arbitrary thread. As Objective-C doesn't have very good thread-management
  // techniques, make sure this method calls its callback on the same thread
  // that it was called on.
  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  [implementation_
      hasContentMatchingTypes:ios_types
            completionHandler:^(NSSet<ContentType>* results) {
              std::set<ClipboardContentType> matching_types;
              for (ContentType type in results) {
                matching_types.insert(
                    ClipboardContentTypeFromContentType(type));
              }
              task_runner->PostTask(
                  FROM_HERE, base::BindOnce(^{
                    std::move(callback_for_block).Run(matching_types);
                  }));
            }];
}

// This value will be nullopt during the brief period
// when the clipboard is updating its cache, which is triggered by a
// pasteboardDidChange notification. It may also be nullopt if the app decides
// it should not return the value of the clipboard, for example if the current
// clipboard contents are too old.
std::optional<std::set<ClipboardContentType>>
ClipboardRecentContentIOS::GetCachedClipboardContentTypes() {
  NSSet<ContentType>* current_content_types =
      [implementation_ cachedClipboardContentTypes];
  if (!current_content_types) {
    return std::nullopt;
  }
  std::set<ClipboardContentType> current_content_types_ios;

  for (ContentType type in current_content_types) {
    current_content_types_ios.insert(ClipboardContentTypeFromContentType(type));
  }

  return current_content_types_ios;
}

void ClipboardRecentContentIOS::GetRecentURLFromClipboard(
    GetRecentURLCallback callback) {
  __block GetRecentURLCallback callback_for_block = std::move(callback);
  // The iOS methods for checking clipboard content call their callbacks on an
  // arbitrary thread. As Objective-C doesn't have very good thread-management
  // techniques, make sure this method calls its callback on the same thread
  // that it was called on.
  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  [implementation_ recentURLFromClipboard:^(NSURL* url) {
    GURL converted_url = net::GURLWithNSURL(url);
    if (!converted_url.is_valid()) {
      task_runner->PostTask(FROM_HERE, base::BindOnce(^{
                              std::move(callback_for_block).Run(std::nullopt);
                            }));
      return;
    }
    task_runner->PostTask(FROM_HERE, base::BindOnce(^{
                            std::move(callback_for_block).Run(converted_url);
                          }));
  }];
}

void ClipboardRecentContentIOS::GetRecentTextFromClipboard(
    GetRecentTextCallback callback) {
  __block GetRecentTextCallback callback_for_block = std::move(callback);
  // The iOS methods for checking clipboard content call their callbacks on an
  // arbitrary thread. As Objective-C doesn't have very good thread-management
  // techniques, make sure this method calls its callback on the same thread
  // that it was called on.
  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  [implementation_ recentTextFromClipboard:^(NSString* text) {
    if (!text) {
      task_runner->PostTask(FROM_HERE, base::BindOnce(^{
                              std::move(callback_for_block).Run(std::nullopt);
                            }));
      return;
    }
    task_runner->PostTask(
        FROM_HERE, base::BindOnce(^{
          std::move(callback_for_block).Run(base::SysNSStringToUTF16(text));
        }));
  }];
}

void ClipboardRecentContentIOS::GetRecentImageFromClipboard(
    GetRecentImageCallback callback) {
  __block GetRecentImageCallback callback_for_block = std::move(callback);
  // The iOS methods for checking clipboard content call their callbacks on an
  // arbitrary thread. As Objective-C doesn't have very good thread-management
  // techniques, make sure this method calls its callback on the same thread
  // that it was called on.
  scoped_refptr<base::SequencedTaskRunner> task_runner =
      base::SequencedTaskRunner::GetCurrentDefault();
  [implementation_ recentImageFromClipboard:^(UIImage* image) {
    if (!image) {
      task_runner->PostTask(FROM_HERE, base::BindOnce(^{
                              std::move(callback_for_block).Run(std::nullopt);
                            }));
      return;
    }
    task_runner->PostTask(
        FROM_HERE, base::BindOnce(^{
          std::move(callback_for_block).Run(gfx::Image(image));
        }));
  }];
}

ClipboardRecentContentIOS::~ClipboardRecentContentIOS() = default;

base::TimeDelta ClipboardRecentContentIOS::GetClipboardContentAge() const {
  return base::Seconds(
      static_cast<int64_t>([implementation_ clipboardContentAge]));
}

void ClipboardRecentContentIOS::SuppressClipboardContent() {
  [implementation_ suppressClipboardContent];
}

void ClipboardRecentContentIOS::ClearClipboardContent() {
  NOTIMPLEMENTED();
  return;
}
