// Clip Library
// Copyright (c) 2015-2020 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.

#include "clip.h"
#include "clip_common.h"
#include "clip_lock_impl.h"

#include <cassert>
#include <vector>
#include <map>

#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/Foundation.h>
#include <AppKit/AppKit.h>

namespace clip {

namespace {

  format g_last_format = 100;
  std::map<std::string, format> g_name_to_format;
  std::map<format, std::string> g_format_to_name;

  bool get_image_from_clipboard(image* output_img,
                                image_spec* output_spec)
  {
    NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
    NSString* result = [pasteboard availableTypeFromArray:
                                [NSArray arrayWithObjects:NSPasteboardTypeTIFF,NSPasteboardTypePNG,nil]];

    if (!result)
      return false;

    NSData* data = [pasteboard dataForType:result];
    if (!data)
      return false;

    NSBitmapImageRep* bitmap = [NSBitmapImageRep imageRepWithData:data];

    if ((bitmap.bitmapFormat & NSBitmapFormatFloatingPointSamples) ||
        (bitmap.planar)) {
      error_handler e = get_error_handler();
      if (e)
        e(ErrorCode::ImageNotSupported);
      return false;
    }

    image_spec spec;
    spec.width = bitmap.pixelsWide;
    spec.height = bitmap.pixelsHigh;
    spec.bits_per_pixel = bitmap.bitsPerPixel;
    spec.bytes_per_row = bitmap.bytesPerRow;

    // We need three samples for Red/Green/Blue
    if (bitmap.samplesPerPixel >= 3) {
      int bits_per_sample = bitmap.bitsPerPixel / bitmap.samplesPerPixel;
      int bits_shift = 0;

      // With alpha
      if (bitmap.alpha) {
        if (bitmap.bitmapFormat & NSBitmapFormatAlphaFirst) {
          spec.alpha_shift = 0;
          bits_shift += bits_per_sample;
        }
        else {
          spec.alpha_shift = 3*bits_per_sample;
        }
      }

      unsigned long* masks = &spec.red_mask;
      unsigned long* shifts = &spec.red_shift;

      // Red/green/blue shifts
      for (unsigned long* shift=shifts; shift<shifts+3; ++shift) {
        *shift = bits_shift;
        bits_shift += bits_per_sample;
      }

      // With alpha
      if (bitmap.alpha) {
        if (bitmap.bitmapFormat & NSBitmapFormatSixteenBitBigEndian ||
            bitmap.bitmapFormat & NSBitmapFormatThirtyTwoBitBigEndian) {
          std::swap(spec.red_shift, spec.alpha_shift);
          std::swap(spec.green_shift, spec.blue_shift);
        }
      }
      // Without alpha
      else {
        if (bitmap.bitmapFormat & NSBitmapFormatSixteenBitBigEndian ||
            bitmap.bitmapFormat & NSBitmapFormatThirtyTwoBitBigEndian) {
          std::swap(spec.red_shift, spec.blue_shift);
        }
      }

      // Calculate all masks
      for (unsigned long* shift=shifts, *mask=masks; shift<shifts+4; ++shift, ++mask)
        *mask = ((1ul<<bits_per_sample)-1ul) << (*shift);

      // Without alpha
      if (!bitmap.alpha)
        spec.alpha_mask = 0;
    }

    if (output_spec) {
      *output_spec = spec;
    }

    if (output_img) {
      unsigned long size = spec.bytes_per_row*spec.height;
      image img(spec);

      std::copy(bitmap.bitmapData,
                bitmap.bitmapData+size, img.data());

      // Convert premultiplied data to unpremultiplied if needed.
      if (bitmap.alpha &&
          bitmap.samplesPerPixel >= 3 &&
          !(bitmap.bitmapFormat & NSBitmapFormatAlphaNonpremultiplied)) {
        details::divide_rgb_by_alpha(
          img,
          true); // hasAlphaGreaterThanZero=true because we have valid alpha information
      }

      std::swap(*output_img, img);
    }

    return true;
  }

}

lock::impl::impl(void*) : m_locked(true) {
}

lock::impl::~impl() {
}

bool lock::impl::clear() {
  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
  [pasteboard clearContents];
  return true;
}

bool lock::impl::is_convertible(format f) const {
  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
  NSString* result = nil;

  if (f == text_format()) {
    result = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSPasteboardTypeString]];
  }
  else if (f == image_format()) {
    result = [pasteboard availableTypeFromArray:
      [NSArray arrayWithObjects:NSPasteboardTypeTIFF,NSPasteboardTypePNG,nil]];
  }
  else {
    auto it = g_format_to_name.find(f);
    if (it != g_format_to_name.end()) {
      const std::string& name = it->second;
      NSString* string = [[NSString alloc] initWithBytesNoCopy:(void*)name.c_str()
                                                        length:name.size()
                                                      encoding:NSUTF8StringEncoding
                                                  freeWhenDone:NO];
      result = [pasteboard availableTypeFromArray:[NSArray arrayWithObject:string]];
    }
  }

  return (result ? true: false);
}

bool lock::impl::set_data(format f, const char* buf, size_t len) {
  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];

  if (f == text_format()) {
    NSString* string = [[NSString alloc] initWithBytesNoCopy:(void*)buf
                                                      length:len
                                                    encoding:NSUTF8StringEncoding
                                                freeWhenDone:NO];
    [pasteboard setString:string forType:NSPasteboardTypeString];
    return true;
  }
  else {
    auto it = g_format_to_name.find(f);
    if (it != g_format_to_name.end()) {
      const std::string& formatName = it->second;
      NSString* typeString = [[NSString alloc]
                               initWithBytesNoCopy:(void*)formatName.c_str()
                                            length:formatName.size()
                                          encoding:NSUTF8StringEncoding
                                      freeWhenDone:NO];
      NSData* data = [NSData dataWithBytesNoCopy:(void*)buf
                                          length:len
                                      freeWhenDone:NO];

      if ([pasteboard setData:data forType:typeString])
        return true;
    }
  }
  return false;
}

bool lock::impl::get_data(format f, char* buf, size_t len) const {
  assert(buf);
  if (!buf || !is_convertible(f))
    return false;

  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];

  if (f == text_format()) {
    NSString* string = [pasteboard stringForType:NSPasteboardTypeString];
    int reqsize = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1;

    assert(reqsize <= len);
    if (reqsize > len) {
      // Buffer is too small
      return false;
    }

    if (reqsize == 0)
      return true;

    memcpy(buf, [string UTF8String], reqsize);
    return true;
  }

  auto it = g_format_to_name.find(f);
  if (it == g_format_to_name.end())
    return false;

  const std::string& formatName = it->second;
  NSString* typeString =
    [[NSString alloc] initWithBytesNoCopy:(void*)formatName.c_str()
                                   length:formatName.size()
                                 encoding:NSUTF8StringEncoding
                             freeWhenDone:NO];

  NSData* data = [pasteboard dataForType:typeString];
  if (!data)
    return false;

  [data getBytes:buf length:len];
  return true;
}

size_t lock::impl::get_data_length(format f) const {
  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];

  if (f == text_format()) {
    NSString* string = [pasteboard stringForType:NSPasteboardTypeString];
    return [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]+1;
  }

  auto it = g_format_to_name.find(f);
  if (it == g_format_to_name.end())
    return 0;

  const std::string& formatName = it->second;
  NSString* typeString =
    [[NSString alloc] initWithBytesNoCopy:(void*)formatName.c_str()
                                   length:formatName.size()
                                 encoding:NSUTF8StringEncoding
                             freeWhenDone:NO];

  NSData* data = [pasteboard dataForType:typeString];
  if (!data)
    return 0;

  return data.length;
}

bool lock::impl::set_image(const image& image) {
  NSPasteboard* pasteboard = [NSPasteboard generalPasteboard];
  const image_spec& spec = image.spec();

  NSBitmapFormat bitmapFormat = 0;
  int samples_per_pixel = 0;
  if (spec.alpha_mask) {
    samples_per_pixel = 4;
    if (spec.alpha_shift == 0)
      bitmapFormat |= NSBitmapFormatAlphaFirst;
    bitmapFormat |= NSBitmapFormatAlphaNonpremultiplied;
  }
  else if (spec.red_mask || spec.green_mask || spec.blue_mask) {
    samples_per_pixel = 3;
  }
  else {
    samples_per_pixel = 1;
  }

  if (spec.bits_per_pixel == 32)
    bitmapFormat |= NSBitmapFormatThirtyTwoBitLittleEndian;
  else if (spec.bits_per_pixel == 16)
    bitmapFormat |= NSBitmapFormatSixteenBitLittleEndian;

  std::vector<unsigned char*> planes(1);
  planes[0] = (unsigned char*)image.data();

  NSBitmapImageRep* bitmap =
    [[NSBitmapImageRep alloc]
      initWithBitmapDataPlanes:&planes[0]
                    pixelsWide:spec.width
                    pixelsHigh:spec.height
                 bitsPerSample:spec.bits_per_pixel / samples_per_pixel
               samplesPerPixel:samples_per_pixel
                      hasAlpha:(spec.alpha_mask ? YES: NO)
                      isPlanar:NO
                colorSpaceName:NSDeviceRGBColorSpace
                  bitmapFormat:bitmapFormat
                   bytesPerRow:spec.bytes_per_row
                  bitsPerPixel:spec.bits_per_pixel];
  if (!bitmap)
    return false;

  NSData* data = bitmap.TIFFRepresentation;
  if (!data)
    return false;

  if ([pasteboard setData:data forType:NSPasteboardTypeTIFF])
    return true;

  return false;
}

bool lock::impl::get_image(image& img) const {
  return get_image_from_clipboard(&img, nullptr);
}

bool lock::impl::get_image_spec(image_spec& spec) const {
  return get_image_from_clipboard(nullptr, &spec);
}

format register_format(const std::string& name) {
  // Check if the format is already registered
  auto it = g_name_to_format.find(name);
  if (it != g_name_to_format.end())
    return it->second;

  format new_format = g_last_format++;
  g_name_to_format[name] = new_format;
  g_format_to_name[new_format] = name;
  return new_format;
}

} // namespace clip
