use crate::clipboard_rs::common::Result;
#[cfg(feature = "image")]
use crate::clipboard_rs::common::{RustImage, RustImageData};
use crate::clipboard_rs::{
  Clipboard, ClipboardContent, ClipboardHandler, ClipboardWatcher, ContentFormat,
};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::AllocAnyThread;
use objc2::ClassType;
use objc2::{rc::autoreleasepool, runtime::ProtocolObject};
use objc2_app_kit::{
  NSImage, NSPasteboard, NSPasteboardItem, NSPasteboardType, NSPasteboardTypeFileURL,
  NSPasteboardTypeHTML, NSPasteboardTypePNG, NSPasteboardTypeRTF, NSPasteboardTypeString,
  NSPasteboardTypeTIFF,
};
use objc2_foundation::{NSArray, NSData, NSString, NSURL};
use std::ffi::c_void;
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use std::vec;

pub struct ClipboardContext {
  pasteboard: Retained<NSPasteboard>,
}

pub struct ClipboardWatcherContext<T: ClipboardHandler> {
  pasteboard: Retained<NSPasteboard>,
  handlers: Vec<T>,
  stop_signal: Sender<()>,
  stop_receiver: Receiver<()>,
  running: bool,
}

unsafe impl<T: ClipboardHandler> Send for ClipboardWatcherContext<T> {}

fn general_pasteboard() -> Result<Retained<NSPasteboard>> {
  let pasteboard: *mut AnyObject =
    unsafe { objc2::msg_send![NSPasteboard::class(), generalPasteboard] };
  let pasteboard = unsafe { Retained::retain_autoreleased(pasteboard) }
    .ok_or("NSPasteboard generalPasteboard unavailable")?;
  Ok(unsafe { Retained::cast_unchecked(pasteboard) })
}

impl<T: ClipboardHandler> ClipboardWatcherContext<T> {
  pub fn new() -> Result<Self> {
    let ns_pasteboard = general_pasteboard()?;
    let (tx, rx) = mpsc::channel();
    Ok(ClipboardWatcherContext {
      pasteboard: ns_pasteboard,
      handlers: Vec::new(),
      stop_signal: tx,
      stop_receiver: rx,
      running: false,
    })
  }
}

impl<T: ClipboardHandler> ClipboardWatcher<T> for ClipboardWatcherContext<T> {
  fn add_handler(&mut self, handler: T) -> &mut Self {
    self.handlers.push(handler);
    self
  }

  fn start_watch(&mut self) {
    if self.running {
      println!("already start watch!");
      return;
    }
    if self.handlers.is_empty() {
      println!("no handler, no need to start watch!");
      return;
    }
    self.running = true;
    let mut last_change_count = self.pasteboard.changeCount();
    loop {
      // if receive stop signal, break loop
      if self
        .stop_receiver
        .recv_timeout(Duration::from_millis(500))
        .is_ok()
      {
        break;
      }
      let change_count = self.pasteboard.changeCount();
      if last_change_count == 0 {
        last_change_count = change_count;
      } else if change_count != last_change_count {
        self
          .handlers
          .iter_mut()
          .for_each(|handler| handler.on_clipboard_change());
        last_change_count = change_count;
      }
    }
    self.running = false;
  }

  fn get_shutdown_channel(&self) -> WatcherShutdown {
    WatcherShutdown {
      stop_signal: self.stop_signal.clone(),
    }
  }
}

impl ClipboardContext {
  pub fn new() -> Result<ClipboardContext> {
    let ns_pasteboard = general_pasteboard()?;
    let clipboard_ctx = ClipboardContext {
      pasteboard: ns_pasteboard,
    };
    Ok(clipboard_ctx)
  }

  fn plain(&self, r#type: &NSPasteboardType) -> Result<String> {
    autoreleasepool(|_| {
      let contents = self
        .pasteboard
        .pasteboardItems()
        .ok_or("NSPasteboard#pasteboardItems errored")?;
      for item in contents {
        if let Some(string) = item.stringForType(r#type) {
          return Ok(string.to_string());
        }
      }
      Err("No string found".into())
    })
  }

  fn set_files(&self, files: &[String]) -> Result<()> {
    autoreleasepool(|_| {
      // Build NSArray<NSURL> and write via writeObjects for better compatibility
      let urls: Vec<Retained<NSURL>> = files
        .iter()
        .filter_map(|file_path| {
          // Normalize to local filesystem path, and verify it exists
          let local_path = if file_path.starts_with("file://") {
            file_path.trim_start_matches("file://").to_string()
          } else {
            file_path.clone()
          };
          if !std::path::Path::new(&local_path).exists() {
            return None;
          }
          let ns_path = NSString::from_str(&local_path);
          // NSURL::fileURLWithPath returns a retained NSURL
          let url = NSURL::fileURLWithPath(&ns_path);
          Some(url)
        })
        .collect();
      if urls.is_empty() {
        return Err("no valid files".into());
      }
      let write_objects = NSArray::from_retained_slice(
        &urls
          .iter()
          .map(|u| ProtocolObject::from_retained(u.clone()))
          .collect::<Vec<_>>(),
      );
      if !self.pasteboard.writeObjects(&write_objects) {
        return Err("writeObjects failed for files".into());
      }
      Ok(())
    })
  }

  // learn from https://github.com/zed-industries/zed/blob/79c1003b344ee513cf97ee8313c38c7c3f02c916/crates/gpui/src/platform/mac/platform.rs#L793
  fn write_to_clipboard(&self, data: &[ClipboardContent], with_clear: bool) -> Result<()> {
    if with_clear {
      self.pasteboard.clearContents();
    }
    autoreleasepool(|_| {
      // we create one NSPasteboardItem for all representations of the same content
      let item = NSPasteboardItem::new();
      let mut has_content_other_than_files = false;

      for d in data {
        match d {
          ClipboardContent::Text(text) => {
            item.setString_forType(&NSString::from_str(text), unsafe { NSPasteboardTypeString });
            has_content_other_than_files = true;
          }
          ClipboardContent::Rtf(rtf) => {
            let rtf_data =
              unsafe { NSData::dataWithBytes_length(rtf.as_ptr() as *const c_void, rtf.len()) };
            item.setData_forType(&rtf_data, unsafe { NSPasteboardTypeRTF });
            has_content_other_than_files = true;
          }
          ClipboardContent::Html(html) => {
            item.setString_forType(&NSString::from_str(html), unsafe { NSPasteboardTypeHTML });
            has_content_other_than_files = true;
          }
          #[cfg(feature = "image")]
          ClipboardContent::Image(image) => {
            if let Ok(png_buffer) = image.to_png() {
              let bytes = png_buffer.get_bytes();
              let ns_data =
                unsafe { NSData::dataWithBytes_length(bytes.as_ptr() as *mut c_void, bytes.len()) };
              item.setData_forType(&ns_data, unsafe { NSPasteboardTypePNG });
              has_content_other_than_files = true;
            };
          }
          ClipboardContent::Files(files) => {
            // Files are set seperately
            let _ = self.set_files(files);
          }
          ClipboardContent::Other(format, buffer) => {
            let ns_data =
              unsafe { NSData::dataWithBytes_length(buffer.as_ptr() as *mut c_void, buffer.len()) };
            item.setData_forType(&ns_data, &NSString::from_str(format));
            has_content_other_than_files = true;
          }
        }
      }
      if has_content_other_than_files {
        let write_objects = NSArray::from_retained_slice(&[ProtocolObject::from_retained(item)]);
        if !self.pasteboard.writeObjects(&write_objects) {
          return Err("writeObjects failed");
        }
      }
      Ok(())
    })?;
    Ok(())
  }
}

unsafe impl Send for ClipboardContext {}

unsafe impl Sync for ClipboardContext {}

impl Clipboard for ClipboardContext {
  fn available_formats(&self) -> Result<Vec<String>> {
    let types = self
      .pasteboard
      .types()
      .ok_or("NSPasteboard#types errored")?;
    let res = types.iter().map(|t| t.to_string()).collect();
    Ok(res)
  }

  fn has(&self, format: ContentFormat) -> bool {
    match format {
      ContentFormat::Text => {
        let types = NSArray::arrayWithObject(unsafe { NSPasteboardTypeString });
        // https://developer.apple.com/documentation/appkit/nspasteboard/1526078-availabletypefromarray?language=objc
        // The first pasteboard type in types that is available on the pasteboard, or nil if the receiver does not contain any of the types in types.
        // self.clipboard.availableTypeFromArray(types)
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
      ContentFormat::Rtf => {
        let types = NSArray::arrayWithObject(unsafe { NSPasteboardTypeRTF });
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
      ContentFormat::Html => {
        // Currently only judge whether there is a public.html format
        let types = NSArray::arrayWithObject(unsafe { NSPasteboardTypeHTML });
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
      #[cfg(feature = "image")]
      ContentFormat::Image => {
        // Currently only judge whether there is a png format
        let types = NSArray::from_retained_slice(&[
          unsafe { NSPasteboardTypePNG }.to_owned(),
          unsafe { NSPasteboardTypeTIFF }.to_owned(),
        ]);
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
      ContentFormat::Files => {
        let types = NSArray::arrayWithObject(unsafe { NSPasteboardTypeFileURL });
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
      ContentFormat::Other(format) => {
        let types = NSArray::from_retained_slice(&[NSString::from_str(&format)]);
        self.pasteboard.availableTypeFromArray(&types).is_some()
      }
    }
  }

  fn clear(&self) -> Result<()> {
    self.pasteboard.clearContents();
    Ok(())
  }

  fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
    if let Some(data) = self.pasteboard.dataForType(&NSString::from_str(format)) {
      return Ok(data.to_vec());
    }
    Err("no data".into())
  }

  fn get_text(&self) -> Result<String> {
    self.plain(unsafe { NSPasteboardTypeString })
  }

  fn get_rich_text(&self) -> Result<String> {
    self.plain(unsafe { NSPasteboardTypeRTF })
  }

  fn get_html(&self) -> Result<String> {
    self.plain(unsafe { NSPasteboardTypeHTML })
  }

  #[cfg(feature = "image")]
  fn get_image(&self) -> Result<RustImageData> {
    autoreleasepool(|_| {
      let png_data = self.pasteboard.dataForType(unsafe { NSPasteboardTypePNG });
      if let Some(data) = png_data {
        return RustImageData::from_bytes(&data.to_vec());
      };
      // if no png data, read NSImage;
      let ns_image = NSImage::initWithPasteboard(NSImage::alloc(), &self.pasteboard);
      if let Some(image) = ns_image {
        let tiff_data = image.TIFFRepresentation();
        if let Some(data) = tiff_data {
          return RustImageData::from_bytes(&data.to_vec());
        }
      };
      Err("no image data".into())
    })
  }

  fn get_files(&self) -> Result<Vec<String>> {
    autoreleasepool(|_| {
      let mut res = vec![];
      // 使用 readObjectsForClasses 读取 NSURL（文件 URL）
      // 相当于 Objective-C: [pasteboard readObjectsForClasses:@[[NSURL class]] options:nil]
      let classes = NSArray::arrayWithObject(NSURL::class());
      let objects = unsafe {
        self
          .pasteboard
          .readObjectsForClasses_options(&classes, None)
      };
      if let Some(objects) = objects {
        for any_obj in objects {
          // 尝试将返回对象视为 NSURL
          if let Ok(url) = any_obj.downcast::<NSURL>() {
            // 只接受文件 URL
            if url.isFileURL() {
              // 优先使用 path（去掉 file:// 前缀后的本地路径）
              if let Some(path) = url.path() {
                res.push(path.to_string());
              } else {
                // 兜底使用 absoluteString，再去掉前缀
                let abs = if let Some(s) = url.absoluteString() {
                  s.to_string()
                } else {
                  String::new()
                };
                let file_path = if abs.starts_with("file://") {
                  abs.strip_prefix("file://").unwrap_or(&abs).to_string()
                } else {
                  abs
                };
                res.push(file_path);
              }
            }
          }
        }
      }
      if res.is_empty() {
        return Err("no files".into());
      }
      Ok(res)
    })
  }

  fn get(&self, formats: &[ContentFormat]) -> Result<Vec<ClipboardContent>> {
    autoreleasepool(|_| {
      let contents = self
        .pasteboard
        .pasteboardItems()
        .ok_or("NSPasteboard#pasteboardItems errored")?;
      let mut results = Vec::new();
      for format in formats {
        for item in contents.iter() {
          match format {
            ContentFormat::Text => {
              if let Some(string) = item.stringForType(unsafe { NSPasteboardTypeString }) {
                results.push(ClipboardContent::Text(string.to_string()));
                break;
              }
            }
            ContentFormat::Rtf => {
              if let Some(string) = item.stringForType(unsafe { NSPasteboardTypeRTF }) {
                results.push(ClipboardContent::Rtf(string.to_string()));
                break;
              }
            }
            ContentFormat::Html => {
              if let Some(string) = item.stringForType(unsafe { NSPasteboardTypeHTML }) {
                results.push(ClipboardContent::Html(string.to_string()));
                break;
              }
            }
            #[cfg(feature = "image")]
            ContentFormat::Image => {
              if let Ok(image) = self.get_image() {
                results.push(ClipboardContent::Image(image));
                break;
              }
            }
            ContentFormat::Files => {
              if let Ok(files) = self.get_files() {
                results.push(ClipboardContent::Files(files));
                break;
              }
            }
            ContentFormat::Other(format_name) => {
              if let Some(data) = item.dataForType(&NSString::from_str(format_name)) {
                results.push(ClipboardContent::Other(
                  format_name.to_string(),
                  data.to_vec(),
                ));
                break;
              }
            }
          }
        }
      }
      Ok(results)
    })
  }

  fn set_buffer(&self, format: &str, buffer: Vec<u8>) -> Result<()> {
    self.write_to_clipboard(&[ClipboardContent::Other(format.to_owned(), buffer)], true)
  }

  fn set_text(&self, text: String) -> Result<()> {
    self.write_to_clipboard(&[ClipboardContent::Text(text)], true)
  }

  fn set_rich_text(&self, text: String) -> Result<()> {
    self.write_to_clipboard(&[ClipboardContent::Rtf(text)], true)
  }

  fn set_html(&self, html: String) -> Result<()> {
    self.write_to_clipboard(&[ClipboardContent::Html(html)], true)
  }

  #[cfg(feature = "image")]
  fn set_image(&self, image: RustImageData) -> Result<()> {
    self.write_to_clipboard(&[ClipboardContent::Image(image)], true)
  }

  fn set_files(&self, files: Vec<String>) -> Result<()> {
    if files.is_empty() {
      return Err("file list is empty".into());
    }
    let _ = self.clear();
    self.set_files(&files)
  }

  fn set(&self, contents: Vec<ClipboardContent>) -> Result<()> {
    if contents.is_empty() {
      return Err(
        "contents is empty, if you want to clear clipboard, please use clear method".into(),
      );
    }
    self.write_to_clipboard(&contents, true)
  }
}

pub struct WatcherShutdown {
  stop_signal: Sender<()>,
}

impl Drop for WatcherShutdown {
  fn drop(&mut self) {
    let _ = self.stop_signal.send(());
  }
}
