// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

#include "platform/globals.h"
#if defined(DART_HOST_OS_MACOS)

#include "bin/file_system_watcher.h"

#if !DART_HOST_OS_IOS

#include <CoreServices/CoreServices.h>  // NOLINT
#include <dispatch/dispatch.h>
#include <errno.h>   // NOLINT
#include <fcntl.h>   // NOLINT
#include <unistd.h>  // NOLINT

#include "bin/eventhandler.h"
#include "bin/fdutils.h"
#include "bin/file.h"
#include "bin/lockers.h"
#include "bin/namespace.h"
#include "bin/socket.h"
#include "bin/thread.h"
#include "platform/signal_blocker.h"

namespace dart {
namespace bin {

union FSEvent {
  struct {
    uint32_t exists;
    uint32_t flags;
    char path[PATH_MAX];
  } data;
  uint8_t bytes[PATH_MAX + 8];
};

// A helper for creating Dart_CObject array using a single allocation.
//
// We can't use CObject helpers because those rely on Dart_ScopeAllocate.
namespace {

template <typename T>
struct SetCObjectValue;

template <>
struct SetCObjectValue<const char*> {
  static void Assign(Dart_CObject* o, const char* str) {
    o->type = Dart_CObject_kString;
    o->value.as_string = str;
  }
};

template <>
struct SetCObjectValue<char*> {
  static void Assign(Dart_CObject* o, const char* str) {
    o->type = Dart_CObject_kString;
    o->value.as_string = str;
  }
};

template <>
struct SetCObjectValue<int64_t> {
  static void Assign(Dart_CObject* o, int64_t v) {
    o->type = Dart_CObject_kInt64;
    o->value.as_int64 = v;
  }
};

template <typename... Ts>
Dart_CObject* CreateCObjectArray(Ts... elements) {
  const auto length = sizeof...(elements);
  auto array = static_cast<Dart_CObject*>(
      malloc(sizeof(Dart_CObject) +
             (sizeof(Dart_CObject*) + sizeof(Dart_CObject)) * length));
  array->type = Dart_CObject_kArray;
  array->value.as_array.values = reinterpret_cast<Dart_CObject**>(array + 1);
  array->value.as_array.length = length;
  for (uintptr_t i = 0; i < length; i++) {
    array->value.as_array.values[i] =
        reinterpret_cast<Dart_CObject*>(array->value.as_array.values + length) +
        i;
  }

  int index = 0;
  (
      [&] {
        SetCObjectValue<Ts>::Assign(array->value.as_array.values[index],
                                    elements);
        ++index;
      }(),
      ...);

  return array;
}

}  // namespace

class Node {
 public:
  Node(Dart_Port port, char* base_path, bool recursive)
      : port_(port),
        base_path_length_(strlen(base_path)),
        path_ref_(CFStringCreateWithCString(nullptr,
                                            base_path,
                                            kCFStringEncodingUTF8)),
        recursive_(recursive),
        ref_(nullptr) {
    Start();
  }

  ~Node() {
    // This is invoked outside of [Callback] execution because
    // [context.release] callback is invoked when [FSEventStream] is
    // deallocated, the same [FSEventStream] that [Callback] gets a reference
    // to during its execution. [Callback] holding a reference prevents stream
    // from deallocation.
    CFRelease(path_ref_);
  }

  void set_ref(FSEventStreamRef ref) { ref_ = ref; }

  void Start() {
    FSEventStreamContext context;
    memset(&context, 0, sizeof(context));
    context.info = reinterpret_cast<void*>(this);
    context.release = [](const void* info) {
      reinterpret_cast<Node*>(const_cast<void*>(info))->NotifyStopped();
    };
    CFArrayRef array = CFArrayCreate(
        nullptr, reinterpret_cast<const void**>(&path_ref_), 1, nullptr);
    FSEventStreamRef ref = FSEventStreamCreate(
        nullptr, Callback, &context, array, kFSEventStreamEventIdSinceNow, 0.10,
        kFSEventStreamCreateFlagFileEvents);
    CFRelease(array);

    set_ref(ref);

    FSEventStreamSetDispatchQueue(ref_, notification_queue_);
    FSEventStreamStart(ref_);
    FSEventStreamFlushSync(ref_);
  }

  void Stop() {
    FSEventStreamStop(ref_);
    FSEventStreamInvalidate(ref_);
    FSEventStreamRelease(ref_);
    {
      MonitorLocker lock(&monitor_);
      while (running_) {
        lock.Wait();
      }
    }
  }

  intptr_t base_path_length() const { return base_path_length_; }
  bool recursive() const { return recursive_; }

  static Node* Watch(Dart_Port port,
                     const char* path,
                     int events,
                     bool recursive) {
    char base_path[PATH_MAX];
    realpath(path, base_path);

    return new Node(port, base_path, recursive);
  }

  static void Unwatch(Node* node) {
    node->Stop();
    delete node;
  }

  static void InitOnce() {
    notification_queue_ =
        dispatch_queue_create("dev.dart.fsevents", DISPATCH_QUEUE_SERIAL);
  }

  static void Cleanup() {
    // We want to make sure that no Node is active, these should have all been
    // destroyed.
    dispatch_release(notification_queue_);
  }

 private:
  void NotifyStopped() {
    MonitorLocker lock(&monitor_);
    running_ = false;
    lock.Notify();
  }

  static void Callback(ConstFSEventStreamRef ref,
                       void* client,
                       size_t num_events,
                       void* event_paths,
                       const FSEventStreamEventFlags event_flags[],
                       const FSEventStreamEventId event_ids[]) {
    if (FileSystemWatcher::delayed_filewatch_callback()) {
      // Used in tests to highlight race between callback invocation
      // and unwatching the file path, Node destruction
      TimerUtils::Sleep(1000 /* ms */);
    }
    Node* node = static_cast<Node*>(client);

    // Can't use CObject helpers because they expect Dart_ScopeAllocate to work
    // and this thread is not attached to any isolate or native message handler.
    Dart_CObject events;
    events.type = Dart_CObject_kArray;
    events.value.as_array.values =
        static_cast<Dart_CObject**>(malloc(sizeof(Dart_CObject*) * num_events));
    events.value.as_array.length = 0;
    for (size_t i = 0; i < num_events; i++) {
      char* path = reinterpret_cast<char**>(event_paths)[i];
      char* relative_path = path;

      relative_path += node->base_path_length();
      // If path is longer than the base, skip next character ('/').
      if (relative_path[0] != '\0') {
        relative_path += 1;
      }
      if (!node->recursive() && (strstr(relative_path, "/") != nullptr)) {
        continue;
      }

      const bool is_self = relative_path[0] == '\0';
      const bool path_exists =
          File::GetType(nullptr, path, false) != File::kDoesNotExist;

      events.value.as_array.values[events.value.as_array.length++] =
          CreateCObjectArray(
              /*flags=*/ConvertEventFlags(event_flags[i], is_self, path_exists),
              /*cookie=*/static_cast<int64_t>(0), relative_path,
              /*path_id=*/reinterpret_cast<int64_t>(node));
    }

    if (events.value.as_array.length != 0) {
      Dart_PostCObject(node->port_, &events);
    }

    for (int i = 0; i < events.value.as_array.length; i++) {
      free(events.value.as_array.values[i]);
    }
    free(events.value.as_array.values);
  }

  static int64_t ConvertEventFlags(FSEventStreamEventFlags flags,
                                   bool is_self,
                                   bool path_exists) {
    int64_t mask = 0;
    if ((flags & kFSEventStreamEventFlagItemRenamed) != 0) {
      if (is_self) {
        // The moved path is the path being watched.
        mask |= FileSystemWatcher::kDeleteSelf;
      } else if (path_exists) {
        mask |= FileSystemWatcher::kCreate;
      } else {
        mask |= FileSystemWatcher::kDelete;
      }
    }
    if ((flags & kFSEventStreamEventFlagItemModified) != 0) {
      mask |= FileSystemWatcher::kModifyContent;
    }
    if ((flags & kFSEventStreamEventFlagItemXattrMod) != 0) {
      mask |= FileSystemWatcher::kModifyAttribute;
    }
    if ((flags & kFSEventStreamEventFlagItemCreated) != 0) {
      mask |= FileSystemWatcher::kCreate;
    }
    if ((flags & kFSEventStreamEventFlagItemIsDir) != 0) {
      mask |= FileSystemWatcher::kIsDir;
    }
    if ((flags & kFSEventStreamEventFlagItemRemoved) != 0) {
      if (is_self) {
        // The removed path is the path being watched.
        mask |= FileSystemWatcher::kDeleteSelf;
      } else {
        mask |= FileSystemWatcher::kDelete;
      }
    }
    return mask;
  }

  static dispatch_queue_t notification_queue_;

  Dart_Port port_;
  intptr_t base_path_length_;
  CFStringRef path_ref_;
  bool recursive_;
  FSEventStreamRef ref_;
  Monitor monitor_;
  bool running_ = true;

  DISALLOW_COPY_AND_ASSIGN(Node);
};

dispatch_queue_t Node::notification_queue_;

bool FileSystemWatcher::IsSupported() {
  return true;
}

void FileSystemWatcher::InitOnce() {
  Node::InitOnce();
}

void FileSystemWatcher::Cleanup() {
  Node::Cleanup();
}

intptr_t FileSystemWatcher::Init() {
  return 0;
}

intptr_t FileSystemWatcher::WatchPath(intptr_t id,
                                      Namespace* namespc,
                                      const char* path,
                                      int events,
                                      bool recursive) {
  return reinterpret_cast<intptr_t>(
      Node::Watch(static_cast<Dart_Port>(id), path, events, recursive));
}

void FileSystemWatcher::UnwatchPath(intptr_t id, intptr_t path_id) {
  USE(id);
  Node::Unwatch(reinterpret_cast<Node*>(path_id));
}

void FileSystemWatcher::DestroyWatch(void* ptr) {
  intptr_t path_id = reinterpret_cast<intptr_t>(ptr);
  FileSystemWatcher::UnwatchPath(0, path_id);
}

intptr_t FileSystemWatcher::GetSocketId(intptr_t id, intptr_t path_id) {
  // This API should not be called. We are communicating over ports instead.
  return -1;
}

Dart_Handle FileSystemWatcher::ReadEvents(intptr_t id, intptr_t path_id) {
  // This API should not be called. We are communicating over ports instead.
  return DartUtils::NewDartOSError();
}

}  // namespace bin
}  // namespace dart

#else  // !DART_HOST_OS_IOS

namespace dart {
namespace bin {

// FSEvents are unavailable on iOS. Stub out related methods
Dart_Handle FileSystemWatcher::ReadEvents(intptr_t id, intptr_t path_id) {
  return DartUtils::NewDartOSError();
}

intptr_t FileSystemWatcher::GetSocketId(intptr_t id, intptr_t path_id) {
  return -1;
}

bool FileSystemWatcher::IsSupported() {
  return false;
}

void FileSystemWatcher::UnwatchPath(intptr_t id, intptr_t path_id) {}

void FileSystemWatcher::DestroyWatch(void* ptr) {}

void FileSystemWatcher::InitOnce() {}

void FileSystemWatcher::Cleanup() {}

intptr_t FileSystemWatcher::Init() {
  return -1;
}

intptr_t FileSystemWatcher::WatchPath(intptr_t id,
                                      Namespace* namespc,
                                      const char* path,
                                      int events,
                                      bool recursive) {
  return -1;
}

}  // namespace bin
}  // namespace dart

#endif  // !DART_HOST_OS_IOS
#endif  // defined(DART_HOST_OS_MACOS)
