// Copyright 2022 Google LLC
//
// 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
//
//     https://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.

#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCCoreAdapter.h"

#import <Foundation/Foundation.h>

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "connections/core.h"

#import "connections/swift/NearbyCoreAdapter/Sources/GNCAdvertisingOptions+CppConversions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/GNCConnectionOptions+CppConversions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/GNCDiscoveryOptions+CppConversions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/GNCError+Internal.h"
#import "connections/swift/NearbyCoreAdapter/Sources/GNCPayload+CppConversions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCAdvertisingOptions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCConnectionDelegate.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCConnectionOptions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCDiscoveryDelegate.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCDiscoveryOptions.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCError.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCPayload.h"
#import "connections/swift/NearbyCoreAdapter/Sources/Public/NearbyCoreAdapter/GNCPayloadDelegate.h"

using ::nearby::ByteArray;
using ::nearby::connections::AdvertisingOptions;
using ::nearby::connections::ConnectionListener;
using ::nearby::connections::ConnectionOptions;
using ::nearby::connections::ConnectionRequestInfo;
using ::nearby::connections::ConnectionResponseInfo;
using ::nearby::connections::Core;
using ::nearby::connections::DiscoveryListener;
using ::nearby::connections::DiscoveryOptions;
using ::nearby::connections::Payload;
using ::nearby::connections::PayloadListener;
using ::nearby::connections::PayloadProgressInfo;
using ResultListener = ::nearby::connections::ResultCallback;
using ::nearby::connections::ServiceControllerRouter;
using ::nearby::connections::Status;

GNCStatus GNCStatusFromCppStatus(Status status) {
  switch (status.value) {
    case Status::kSuccess:
      return GNCStatusSuccess;
    case Status::kError:
      return GNCStatusError;
    case Status::kOutOfOrderApiCall:
      return GNCStatusOutOfOrderApiCall;
    case Status::kAlreadyHaveActiveStrategy:
      return GNCStatusAlreadyHaveActiveStrategy;
    case Status::kAlreadyAdvertising:
      return GNCStatusAlreadyAdvertising;
    case Status::kAlreadyDiscovering:
      return GNCStatusAlreadyDiscovering;
    case Status::kAlreadyListening:
      return GNCStatusAlreadyListening;
    case Status::kEndpointIoError:
      return GNCStatusEndpointIoError;
    case Status::kEndpointUnknown:
      return GNCStatusEndpointUnknown;
    case Status::kConnectionRejected:
      return GNCStatusConnectionRejected;
    case Status::kAlreadyConnectedToEndpoint:
      return GNCStatusAlreadyConnectedToEndpoint;
    case Status::kNotConnectedToEndpoint:
      return GNCStatusNotConnectedToEndpoint;
    case Status::kBluetoothError:
      return GNCStatusBluetoothError;
    case Status::kBleError:
      return GNCStatusBleError;
    case Status::kWifiLanError:
      return GNCStatusWifiLanError;
    case Status::kPayloadUnknown:
      return GNCStatusPayloadUnknown;
    case Status::kReset:
      return GNCStatusReset;
    case Status::kTimeout:
      return GNCStatusTimeout;
    case Status::kUnknown:
      return GNCStatusUnknown;
    case Status::kNextValue:
      return GNCStatusUnknown;
  }
}

@interface GNCCoreAdapter () {
  std::unique_ptr<Core> _core;
  std::unique_ptr<ServiceControllerRouter> _serviceControllerRouter;
}

@end

@implementation GNCCoreAdapter

+ (GNCCoreAdapter *)shared {
  static dispatch_once_t onceToken;
  static GNCCoreAdapter *shared = nil;
  dispatch_once(&onceToken, ^{
    shared = [[GNCCoreAdapter alloc] init];
  });
  return shared;
}

- (instancetype)init {
  self = [super init];
  if (self) {
    _serviceControllerRouter = std::make_unique<ServiceControllerRouter>();
    _core = std::make_unique<Core>(_serviceControllerRouter.get());
  }
  return self;
}

- (void)dealloc {
  // Make sure Core is deleted before ServiceControllerRouter.
  _core.reset();
  _serviceControllerRouter.reset();
}

- (void)startAdvertisingAsService:(NSString *)serviceID
                     endpointInfo:(NSData *)endpointInfo
                          options:(GNCAdvertisingOptions *)advertisingOptions
                         delegate:(id<GNCConnectionDelegate>)delegate
            withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string service_id = [serviceID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  AdvertisingOptions advertising_options = [advertisingOptions toCpp];

  ConnectionListener listener;
  listener.initiated_cb = ^(const std::string &endpoint_id, const ConnectionResponseInfo &info) {
    NSString *endpointID = @(endpoint_id.c_str());
    NSData *endpointInfo = [NSData dataWithBytes:info.remote_endpoint_info.data()
                                          length:info.remote_endpoint_info.size()];
    NSString *authenticationToken = @(info.authentication_token.c_str());
    [delegate connectedToEndpoint:endpointID
                 withEndpointInfo:endpointInfo
              authenticationToken:authenticationToken];
  };
  listener.accepted_cb = ^(const std::string &endpoint_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate acceptedConnectionToEndpoint:endpointID];
  };
  listener.rejected_cb = ^(const std::string &endpoint_id, Status status) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate rejectedConnectionToEndpoint:endpointID withStatus:GNCStatusFromCppStatus(status)];
  };
  listener.disconnected_cb = ^(const std::string &endpoint_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate disconnectedFromEndpoint:endpointID];
  };

  ConnectionRequestInfo connection_request_info;
  connection_request_info.endpoint_info =
      ByteArray((const char *)endpointInfo.bytes, endpointInfo.length);
  connection_request_info.listener = std::move(listener);

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->StartAdvertising(service_id, advertising_options, connection_request_info,
                          std::move(result));
}

- (void)stopAdvertisingWithCompletionHandler:(void (^)(NSError *error))completionHandler {
  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->StopAdvertising(std::move(result));
}

- (void)startDiscoveryAsService:(NSString *)serviceID
                        options:(GNCDiscoveryOptions *)discoveryOptions
                       delegate:(id<GNCDiscoveryDelegate>)delegate
          withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string service_id = [serviceID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  DiscoveryOptions discovery_options = [discoveryOptions toCpp];

  DiscoveryListener listener;
  listener.endpoint_found_cb = [delegate](const std::string &endpoint_id,
                                          const ByteArray &endpoint_info,
                                          const std::string &service_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    NSData *info = [NSData dataWithBytes:endpoint_info.data() length:endpoint_info.size()];
    [delegate foundEndpoint:endpointID withEndpointInfo:info];
  };
  listener.endpoint_lost_cb = [delegate](const std::string &endpoint_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate lostEndpoint:endpointID];
  };

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->StartDiscovery(service_id, discovery_options, std::move(listener), std::move(result));
}

- (void)stopDiscoveryWithCompletionHandler:(void (^)(NSError *error))completionHandler {
  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->StopDiscovery(std::move(result));
}

- (void)requestConnectionToEndpoint:(NSString *)endpointID
                       endpointInfo:(NSData *)endpointInfo
                            options:(GNCConnectionOptions *)connectionOptions
                           delegate:(id<GNCConnectionDelegate>)delegate
              withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  ConnectionListener listener;
  listener.initiated_cb = ^(const std::string &endpoint_id, const ConnectionResponseInfo &info) {
    NSString *endpointID = @(endpoint_id.c_str());
    NSData *endpointInfo = [NSData dataWithBytes:info.remote_endpoint_info.data()
                                          length:info.remote_endpoint_info.size()];
    NSString *authenticationToken = @(info.authentication_token.c_str());
    [delegate connectedToEndpoint:endpointID
                 withEndpointInfo:endpointInfo
              authenticationToken:authenticationToken];
  };
  listener.accepted_cb = ^(const std::string &endpoint_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate acceptedConnectionToEndpoint:endpointID];
  };
  listener.rejected_cb = ^(const std::string &endpoint_id, Status status) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate rejectedConnectionToEndpoint:endpointID withStatus:GNCStatusFromCppStatus(status)];
  };
  listener.disconnected_cb = ^(const std::string &endpoint_id) {
    NSString *endpointID = @(endpoint_id.c_str());
    [delegate disconnectedFromEndpoint:endpointID];
  };

  ConnectionRequestInfo connection_request_info;
  connection_request_info.endpoint_info =
      ByteArray((const char *)endpointInfo.bytes, endpointInfo.length);
  connection_request_info.listener = std::move(listener);

  ConnectionOptions connection_options = [connectionOptions toCpp];

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->RequestConnection(endpoint_id, connection_request_info, connection_options,
                           std::move(result));
}

- (void)acceptConnectionRequestFromEndpoint:(NSString *)endpointID
                                   delegate:(id<GNCPayloadDelegate>)delegate
                      withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  PayloadListener listener;
  listener.payload_cb = [delegate](absl::string_view endpoint_id, Payload payload) {
    NSString *endpointID = @(std::string(endpoint_id).c_str());
    GNCPayload *gncPayload = [GNCPayload fromCpp:std::move(payload)];
    [delegate receivedPayload:gncPayload fromEndpoint:endpointID];
  };
  listener.payload_progress_cb = [delegate](absl::string_view endpoint_id,
                                            const PayloadProgressInfo &info) {
    NSString *endpointID = @(std::string(endpoint_id).c_str());
    GNCPayloadStatus status;
    switch (info.status) {
      case PayloadProgressInfo::Status::kSuccess:
        status = GNCPayloadStatusSuccess;
        break;
      case PayloadProgressInfo::Status::kFailure:
        status = GNCPayloadStatusFailure;
        break;
      case PayloadProgressInfo::Status::kInProgress:
        status = GNCPayloadStatusInProgress;
        break;
      case PayloadProgressInfo::Status::kCanceled:
        status = GNCPayloadStatusCanceled;
        break;
    }
    [delegate receivedProgressUpdateForPayload:info.payload_id
                                    withStatus:status
                                  fromEndpoint:endpointID
                               bytesTransfered:info.bytes_transferred
                                    totalBytes:info.total_bytes];
  };

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->AcceptConnection(endpoint_id, std::move(listener), std::move(result));
}

- (void)rejectConnectionRequestFromEndpoint:(NSString *)endpointID
                      withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->RejectConnection(endpoint_id, std::move(result));
}

- (void)sendPayload:(GNCPayload *)payload
              toEndpoints:(NSArray<NSString *> *)endpointIDs
    withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::vector<std::string> endpoint_ids;
  endpoint_ids.reserve([endpointIDs count]);
  for (NSString *endpointID in endpointIDs) {
    std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];
    endpoint_ids.push_back(endpoint_id);
  }

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->SendPayload(endpoint_ids, [payload toCpp], std::move(result));
}

- (void)cancelPayload:(int64_t)payloadID
    withCompletionHandler:(void (^)(NSError *error))completionHandler {
  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->CancelPayload(payloadID, std::move(result));
}

- (void)disconnectFromEndpoint:(NSString *)endpointID
         withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->DisconnectFromEndpoint(endpoint_id, std::move(result));
}

- (void)stopAllEndpointsWithCompletionHandler:(void (^)(NSError *error))completionHandler {
  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };
  _core->StopAllEndpoints(std::move(result));
}

- (void)initiateBandwidthUpgrade:(NSString *)endpointID
           withCompletionHandler:(void (^)(NSError *error))completionHandler {
  std::string endpoint_id = [endpointID cStringUsingEncoding:[NSString defaultCStringEncoding]];

  ResultListener result = [completionHandler](Status status) {
    NSError *err = NSErrorFromCppStatus(status);
    if (completionHandler) {
      completionHandler(err);
    }
  };

  _core->InitiateBandwidthUpgrade(endpoint_id, std::move(result));
}

- (NSString *)localEndpointID {
  std::string endpoint_id = _core->GetLocalEndpointId();
  return @(endpoint_id.c_str());
}

@end
