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

#include "services/shape_detection/barcode_detection_impl_mac_vision.h"

#import <Foundation/Foundation.h>
#import <Vision/Vision.h>

#include <vector>

#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "third_party/skia/include/core/SkBitmap.h"

namespace shape_detection {

namespace {

mojom::BarcodeFormat ToBarcodeFormat(NSString* symbology) {
  if ([symbology isEqual:VNBarcodeSymbologyAztec])
    return mojom::BarcodeFormat::AZTEC;
  if ([symbology isEqual:VNBarcodeSymbologyCode128])
    return mojom::BarcodeFormat::CODE_128;
  if ([symbology isEqual:VNBarcodeSymbologyCode39] ||
      [symbology isEqual:VNBarcodeSymbologyCode39Checksum] ||
      [symbology isEqual:VNBarcodeSymbologyCode39FullASCII] ||
      [symbology isEqual:VNBarcodeSymbologyCode39FullASCIIChecksum]) {
    return mojom::BarcodeFormat::CODE_39;
  }
  if ([symbology isEqual:VNBarcodeSymbologyCode93] ||
      [symbology isEqual:VNBarcodeSymbologyCode93i]) {
    return mojom::BarcodeFormat::CODE_93;
  }
  if ([symbology isEqual:VNBarcodeSymbologyDataMatrix])
    return mojom::BarcodeFormat::DATA_MATRIX;
  if ([symbology isEqual:VNBarcodeSymbologyEAN13])
    return mojom::BarcodeFormat::EAN_13;
  if ([symbology isEqual:VNBarcodeSymbologyEAN8])
    return mojom::BarcodeFormat::EAN_8;
  if ([symbology isEqual:VNBarcodeSymbologyITF14] ||
      [symbology isEqual:VNBarcodeSymbologyI2of5] ||
      [symbology isEqual:VNBarcodeSymbologyI2of5Checksum]) {
    return mojom::BarcodeFormat::ITF;
  }
  if ([symbology isEqual:VNBarcodeSymbologyPDF417])
    return mojom::BarcodeFormat::PDF417;
  if ([symbology isEqual:VNBarcodeSymbologyQR])
    return mojom::BarcodeFormat::QR_CODE;
  if ([symbology isEqual:VNBarcodeSymbologyUPCE])
    return mojom::BarcodeFormat::UPC_E;
  return mojom::BarcodeFormat::UNKNOWN;
}

void UpdateSymbologyHint(mojom::BarcodeFormat format,
                         NSMutableArray<VNBarcodeSymbology>* hint) {
  switch (format) {
    case mojom::BarcodeFormat::AZTEC:
      [hint addObject:VNBarcodeSymbologyAztec];
      return;
    case mojom::BarcodeFormat::CODE_128:
      [hint addObject:VNBarcodeSymbologyCode128];
      return;
    case mojom::BarcodeFormat::CODE_39:
      [hint addObjectsFromArray:@[
        VNBarcodeSymbologyCode39, VNBarcodeSymbologyCode39Checksum,
        VNBarcodeSymbologyCode39FullASCII,
        VNBarcodeSymbologyCode39FullASCIIChecksum
      ]];
      return;
    case mojom::BarcodeFormat::CODE_93:
      [hint addObjectsFromArray:@[
        VNBarcodeSymbologyCode93, VNBarcodeSymbologyCode93i
      ]];
      return;
    case mojom::BarcodeFormat::CODABAR:
      return;
    case mojom::BarcodeFormat::DATA_MATRIX:
      [hint addObject:VNBarcodeSymbologyDataMatrix];
      return;
    case mojom::BarcodeFormat::EAN_13:
      [hint addObject:VNBarcodeSymbologyEAN13];
      return;
    case mojom::BarcodeFormat::EAN_8:
      [hint addObject:VNBarcodeSymbologyEAN8];
      return;
    case mojom::BarcodeFormat::ITF:
      [hint addObjectsFromArray:@[
        VNBarcodeSymbologyITF14, VNBarcodeSymbologyI2of5,
        VNBarcodeSymbologyI2of5Checksum
      ]];
      return;
    case mojom::BarcodeFormat::PDF417:
      [hint addObject:VNBarcodeSymbologyPDF417];
      return;
    case mojom::BarcodeFormat::QR_CODE:
      [hint addObject:VNBarcodeSymbologyQR];
      return;
    case mojom::BarcodeFormat::UPC_A:
      return;
    case mojom::BarcodeFormat::UPC_E:
      [hint addObject:VNBarcodeSymbologyUPCE];
      return;
    case mojom::BarcodeFormat::UNKNOWN:
      NOTREACHED();
      return;
  }
}

}  // namespace

BarcodeDetectionImplMacVision::BarcodeDetectionImplMacVision(
    mojom::BarcodeDetectorOptionsPtr options)
    : weak_factory_(this) {
  NSMutableArray<VNBarcodeSymbology>* symbology_hints = [NSMutableArray array];
  for (const auto& hint : options->formats) {
    if (hint == mojom::BarcodeFormat::UNKNOWN) {
      mojo::ReportBadMessage("Formats hint contains UNKNOWN BarcodeFormat.");
      return;
    }

    UpdateSymbologyHint(hint, symbology_hints);
  }
  symbology_hints_ = symbology_hints;

  // The repeating callback will not be run if BarcodeDetectionImplMacVision
  // object has already been destroyed.
  barcodes_async_request_ = VisionAPIAsyncRequestMac::Create(
      [VNDetectBarcodesRequest class],
      base::BindRepeating(&BarcodeDetectionImplMacVision::OnBarcodesDetected,
                          weak_factory_.GetWeakPtr()),
      symbology_hints_);
}

BarcodeDetectionImplMacVision::~BarcodeDetectionImplMacVision() = default;

void BarcodeDetectionImplMacVision::Detect(const SkBitmap& bitmap,
                                           DetectCallback callback) {
  DCHECK(barcodes_async_request_);

  if (!barcodes_async_request_->PerformRequest(bitmap)) {
    std::move(callback).Run({});
    return;
  }

  image_size_ = CGSizeMake(bitmap.width(), bitmap.height());
  // Hold on the callback until async request completes.
  detected_callback_ = std::move(callback);
  // This prevents the Detect function from being called before the
  // VisionAPIAsyncRequestMac completes.
  if (receiver_)  // Can be unbound in unit testing.
    receiver_->PauseIncomingMethodCallProcessing();
}

void BarcodeDetectionImplMacVision::OnBarcodesDetected(VNRequest* request,
                                                       NSError* error) {
  if (receiver_)  // Can be unbound in unit testing.
    receiver_->ResumeIncomingMethodCallProcessing();

  if ([request.results count] == 0 || error) {
    std::move(detected_callback_).Run({});
    return;
  }

  std::vector<mojom::BarcodeDetectionResultPtr> results;
  for (VNBarcodeObservation* const observation in request.results) {
    auto barcode = mojom::BarcodeDetectionResult::New();
    // The coordinates are normalized to the dimensions of the processed image.
    barcode->bounding_box = ConvertCGToGfxCoordinates(
        CGRectMake(observation.boundingBox.origin.x * image_size_.width,
                   observation.boundingBox.origin.y * image_size_.height,
                   observation.boundingBox.size.width * image_size_.width,
                   observation.boundingBox.size.height * image_size_.height),
        image_size_.height);

    // Enumerate corner points starting from top-left in clockwise fashion:
    // https://wicg.github.io/shape-detection-api/#dom-detectedbarcode-cornerpoints
    barcode->corner_points.emplace_back(
        observation.topLeft.x * image_size_.width,
        (1 - observation.topLeft.y) * image_size_.height);
    barcode->corner_points.emplace_back(
        observation.topRight.x * image_size_.width,
        (1 - observation.topRight.y) * image_size_.height);
    barcode->corner_points.emplace_back(
        observation.bottomRight.x * image_size_.width,
        (1 - observation.bottomRight.y) * image_size_.height);
    barcode->corner_points.emplace_back(
        observation.bottomLeft.x * image_size_.width,
        (1 - observation.bottomLeft.y) * image_size_.height);

    barcode->raw_value =
        base::SysNSStringToUTF8(observation.payloadStringValue);

    barcode->format = ToBarcodeFormat(observation.symbology);

    results.push_back(std::move(barcode));
  }
  std::move(detected_callback_).Run(std::move(results));
}

// static
std::vector<shape_detection::mojom::BarcodeFormat>
BarcodeDetectionImplMacVision::GetSupportedSymbologies(
    VisionAPIInterface* vision_api) {
  std::unique_ptr<VisionAPIInterface> scoped_vision_api;
  if (!vision_api) {
    scoped_vision_api = VisionAPIInterface::Create();
    vision_api = scoped_vision_api.get();
  }
  base::flat_set<shape_detection::mojom::BarcodeFormat> results;
  NSArray<NSString*>* symbologies = vision_api->GetSupportedSymbologies();

  results.reserve(symbologies.count);
  for (VNBarcodeSymbology symbology : symbologies) {
    auto converted = ToBarcodeFormat(symbology);
    if (converted == shape_detection::mojom::BarcodeFormat::UNKNOWN) {
      DLOG(WARNING) << "Symbology " << base::SysNSStringToUTF8(symbology)
                    << " unknown to spec.";
      continue;
    }
    results.insert(converted);
  }
  return std::vector<shape_detection::mojom::BarcodeFormat>(results.begin(),
                                                            results.end());
}

NSArray<VNBarcodeSymbology>*
BarcodeDetectionImplMacVision::GetSymbologyHintsForTesting() {
  return symbology_hints_;
}

}  // namespace shape_detection
