# ifdef HAVE_CONFIG_H
#  include "config.h"
# endif

# define APP_NAME "mclip"
# define APP_VERSION "0.1.1"

# ifdef HAVE_UTF8_CONVERSION
#  define USE_UTF8_STRING
# endif

# include <stdexcept>
# include <vector>
# include <string>
# include <iostream>
# include <map>
# include <set>
# include <algorithm>
# include <functional>
# include <sstream>
# include <iterator>
# include <fstream>
# include <errno.h>
# include <locale.h>
# include <time.h>

# include <X11/Xlib.h>
# include <X11/Xatom.h>
# include <X11/Xutil.h>
# include <sys/select.h>
# include <unistd.h>
# ifdef USE_UTF8_STRING
#  include <langinfo.h>
#  include <iconv.h>
# endif

using namespace std;

typedef unsigned char uchar;

bool g_forked;

struct error : public runtime_error
{
  error(const std::string &msg) : runtime_error(msg) {}
};

# define ERROR(spec)                                                \
  {                                                                 \
    ::std::ostringstream ERROR_stream;                              \
    ERROR_stream << spec;                                           \
    throw ::error(ERROR_stream.str());                              \
  }                                                                 \
  /**/

# define DEF_RUNTIME_ERROR(name)                                    \
  struct name : ::std::runtime_error                                \
  {                                                                 \
    name() : ::std::runtime_error(#name) {}                         \
  };                                                                \
  /**/

DEF_RUNTIME_ERROR(no_such_property)
DEF_RUNTIME_ERROR(conversion_failed)
DEF_RUNTIME_ERROR(wait_timeout)
DEF_RUNTIME_ERROR(bad_window)
DEF_RUNTIME_ERROR(x_bad_alloc)

# undef DEF_RUNTIME_ERROR

# ifdef TRACE
#  define TRC cerr
# else
#  define TRC (::null_output())
# endif

struct null_output
{
  template <typename T>
  null_output &operator<<(const T &) { return *this; }
};

struct connection
{
  explicit connection(const char *dpyname = NULL)
  {
    _dpy = XOpenDisplay(dpyname);
    if(!_dpy)
      throw error("failed to open display");
  }
  Atom intern(const string &name)
  {
    cache_type::iterator t = cache.find(name);
    if(t != cache.end())
      return t->second;
    Atom a = XInternAtom(_dpy, name.c_str(), False);
    cache[name] = a;
    return a;
  }
  ~connection()
  {
    XCloseDisplay(_dpy);
  }


  Display *dpy() const { return _dpy; }
private:
  /* NONCOPYABLE */
  connection(const connection &);
  connection &operator=(const connection &);

  Display *_dpy;
  typedef map<string, Atom> cache_type;
  cache_type cache;
};

bool wait_for_read(int fd, int msec)
{
  fd_set rfds;
  FD_ZERO(&rfds);
  FD_SET(fd, &rfds);
  struct timeval timeout;
  timeout.tv_sec = msec / 1000;
  timeout.tv_usec = msec % 1000 * 1000;
  int r;
  while((r = select(fd + 1, &rfds, NULL, NULL, &timeout)) == -1 && errno == EINTR);
  if(r == -1) throw error("select failed");
  return r;
}

struct listener
{
  listener(connection &c, Window w);

  XEvent next()
  {
    XEvent evt;
    XNextEvent(con.dpy(), &evt);
    update_last(evt);
    return evt;
  }
  XEvent next_with_timeout(int msec)
  {
    if(XPending(con.dpy()) || wait_for_read(XConnectionNumber(con.dpy()), msec))
      return next();
    else
      throw wait_timeout();
  }

  Time current_time() const{ return last; }
private:
  /* NONCOPYABLE */
  listener(const listener &);
  listener &operator=(const listener &);

  connection &con;
  Time last;

  void update_last(const XEvent &evt)
  {
    if(evt.type == PropertyNotify)
      last = evt.xproperty.time;
    else if(evt.type == SelectionNotify)
      last = evt.xselection.time;
  }
};

void ignore(const XEvent &evt)
{
  TRC << "ignoring event: type=" << evt.type << "\n";
}

XPropertyEvent wait_for_prop(listener &lis, Window w, Atom prop, int timeout)
{
  while(1)
  {
    XEvent evt = lis.next_with_timeout(timeout);
    if(evt.type == PropertyNotify && evt.xproperty.state == PropertyNewValue && evt.xproperty.window == w && evt.xproperty.atom == prop)
      return evt.xproperty;
    ignore(evt);
  }
}

XSelectionEvent wait_for_selection(listener &lis, int timeout)
{
  while(1)
  {
    XEvent evt = lis.next_with_timeout(timeout);
    if(evt.type == SelectionNotify)
      return evt.xselection;
    ignore(evt);
  }
}

listener::listener(connection &c, Window w)
  : con(c)
{
  XChangeProperty(con.dpy(), w, XA_STRING, XA_STRING, 8, PropModeAppend, (const unsigned char *)"", 0);
  last = CurrentTime;
  wait_for_prop(*this, w, XA_STRING, 10000);
}

string a2s(connection &con, Atom a)
{
  if(a == None)
    return "<<None>>";
  return XGetAtomName(con.dpy(), a);
}

void warn(const string &msg)
{
  TRC << "warning: " << msg << "\n";
}

template <typename T>
struct auto_xptr
{
  auto_xptr() : ptr(NULL) {}
  explicit auto_xptr(T *p) : ptr(p) {}
  auto_xptr(auto_xptr &other) : ptr(other.ptr) { other.ptr = NULL; }
  auto_xptr &operator=(auto_xptr &other) { ptr = other.ptr; other.ptr = NULL; return *this; }
  T &operator[](size_t idx) { check("[]"); return ptr[idx]; }
  T *get() const { return ptr; }
  template <typename U>
  auto_xptr<U> cast() { T *p = ptr; ptr = NULL; return auto_xptr<U>(reinterpret_cast<U *>(p)); }
  ~auto_xptr() { if(ptr) XFree(ptr); }
private:
  T *ptr;
  void check(const char *info) const { if(!ptr) ERROR("auto_xptr: " << info << ": null pointer") }

  template <typename U>
  struct auto_xptr_ref
  {
    explicit auto_xptr_ref(auto_xptr<U> *p) : ref(p) {}
    auto_xptr<U> *ref;
  };
public:
  auto_xptr(auto_xptr_ref<T> ref) : ptr(ref.ref->ptr) { ref.ref->ptr = NULL; }
  template <typename U>
  operator auto_xptr_ref<U>() { return auto_xptr_ref<U>(this); }
};

struct error_checker
{
  error_checker(connection &c)
    :con(c)
  {
    XSync(con.dpy(), False);
    error_ocurred = false;
    old = XSetErrorHandler(handler);
  }
  ~error_checker()
  {
    XSetErrorHandler(old);
  }
  unsigned char check()
  {
    XSync(con.dpy(), False);
    return error_ocurred ? last.error_code : Success;
  }
  void handle()
  {
    if(error_ocurred)
      old(con.dpy(), &last);
  }
private:
  static int handler(Display *dpy, XErrorEvent *ee)
  {
    error_ocurred = true;
    last = *ee;
    return 0;
  }
  static bool error_ocurred;
  static XErrorEvent last;

  int (*old)(Display *, XErrorEvent *);
  connection &con;
};

XErrorEvent error_checker::last;
bool error_checker::error_ocurred;

void check_errors(error_checker &chk)
{
  switch(chk.check())
  {
  case Success:
    break;
  case BadWindow:
    throw bad_window();
  case BadAlloc:
    throw x_bad_alloc();
  default:
    chk.handle();
    break;
  }
}

void set_property(connection &con, Window w, Atom prop, Atom type, int format, unsigned char *data, int nelements)
{
  error_checker chk(con);
  XChangeProperty(con.dpy(), w, prop, type, format, PropModeReplace, data, nelements);
  check_errors(chk);
}

void select_input(connection &con, Window w, long mask)
{
  error_checker chk(con);
  XSelectInput(con.dpy(), w, mask);
  check_errors(chk);
}

bool take_incr(connection &con, Window w, Atom prop)
{
  Atom type;
  int format;
  unsigned long nitems, bytes_after;
  unsigned char *value;
  if(Success != XGetWindowProperty(con.dpy(), w, prop, 0, 0, False, con.intern("INCR"), &type, &format, &nitems, &bytes_after, &value))
    throw no_such_property();
  XFree(value);
  
  if(type == con.intern("INCR"))
  {
    XDeleteProperty(con.dpy(), w, prop);
    return true;
  }
  TRC << "take_incr: type was " << a2s(con, type) << "\n";
  return false;
}

void decode_text_property(connection &con, const XTextProperty &tp, ostream &out)
{
  TRC << "decode_text_property: tp.nitems=" << tp.nitems << "; tp.format=" << tp.format << "; tp.encoding=" << a2s(con, tp.encoding) << "\n";
  char **list;
  int count;
  int r = XmbTextPropertyToTextList(con.dpy(), &tp, &list, &count);
  if(r == XNoMemory)
    throw error("no memory to convert string");
  else if(r == XLocaleNotSupported)
    throw error("current locale not supported");
  else if(r == XConverterNotFound)
    throw error("converter not found");
  else
  {
    if(r != Success)
      warn("could not complete conversion");
    int size = 0;
    for(int i = 0; i < count; i++)
    {
      size += strlen(list[i]);
      out << list[i];
    }
    TRC << "output chunk: r=" << r << "; count=" << count << "; total_size=" << size << "\n";
    XFreeStringList(list);
  }
}

Atom property_type(connection &con, Window w, Atom prop)
{
  Atom type;
  int format;
  unsigned long nitems, bytes_after;
  unsigned char *value;
  if(Success != XGetWindowProperty(con.dpy(), w, prop, 0, 0, False, AnyPropertyType, &type, &format, &nitems, &bytes_after, &value))
    throw no_such_property();
  XFree(value);
  return type;
}

int property_length(connection &con, Window w, Atom prop)
{
  Atom type;
  int format;
  unsigned long nitems, bytes_after;
  unsigned char *value = NULL;
  if(Success != XGetWindowProperty(con.dpy(), w, prop, 0, 0, False, AnyPropertyType, &type, &format, &nitems, &bytes_after, &value))
    throw no_such_property();
  XFree(value);
  return bytes_after;
}

int property_length_z(connection &con, Window w, Atom prop)
{
  try
  {
    return property_length(con, w, prop);
  }
  catch(no_such_property)
  {
    warn("ignoring: no such property");
  }
  return 0;
}

typedef vector<uchar> buf_type;

auto_xptr<uchar> get_whole_property(connection &con, Window w, Atom prop, Atom type, Atom *out_type, int *out_format, unsigned long *out_nitems)
{
  uchar *value;
  unsigned long nbytes;
  int len = property_length(con, w, prop);
  if(Success != XGetWindowProperty(con.dpy(), w, prop, 0, len / 4 + 1, False, type, out_type, out_format, out_nitems, &nbytes, &value))
    throw no_such_property();
  return auto_xptr<uchar>(value);
}

void decode_ctext(connection &con, const uchar *data, int size, ostream &out)
{
  XTextProperty tp;
  tp.value = (uchar *)data;
  tp.encoding = con.intern("COMPOUND_TEXT");
  tp.format = 8;
  tp.nitems = size;
  decode_text_property(con, tp, out);
}

# ifdef USE_UTF8_STRING

struct iconv_w
{
  iconv_w(const char *to, const char *from)
    : t(iconv_open(to, from))
  {
    if(t == (iconv_t)-1)
      ERROR("iconv_open failed: to=" << to << "; from=" << from)
  }
  ~iconv_w() { iconv_close(t); }
  const iconv_t t;
private:
  /* NONCOPYABLE */
  iconv_w(const iconv_w &);
  iconv_w &operator=(const iconv_w &);
};

int utf8_len(unsigned char byte)
{
  return (byte >= 0xfc) + (byte >= 0xf8) + (byte >= 0xf0) + (byte >= 0xe0) + (byte >= 0xc0) + 1;
}

int decode_utf8_partial(iconv_w &iw, const uchar *data, int size, ostream &out)
{
  static const int bufsize = 4096;
  char buf[bufsize];
  char *pin = (char *)data;
  char *pout = buf;
  size_t in_left = size;
  size_t out_left = bufsize;
  while(pin < (char *)data + size)
  {
    if(iconv(iw.t, &pin, &in_left, &pout, &out_left) != (size_t)-1)
      break;
    switch(errno)
    {
    case E2BIG:
      out.write(buf, pout - buf);
      pout = buf;
      out_left = bufsize;
      break;
    case EINVAL:
      goto end;
    case EILSEQ:
      out.write(buf, pout - buf);
      pout = buf;
      out_left = bufsize;
      out << '?';
      int step = utf8_len(*pin);
      pin += step;
      in_left -= step;
      break;
    }
  }
end:
  out.write(buf, pout - buf);
  return min(pin - (char *)data, size);
}

void decode_utf8(const uchar *data, int size, ostream &out)
{
  iconv_w w(nl_langinfo(CODESET), "UTF-8");
  decode_utf8_partial(w, data, size, out);
}

pair<int, int> encode_utf8_partial(iconv_w &iw, const char *src, int srcsize, uchar *dest, int destsize)
{
  char *pin = (char *)src;
  char *pout = (char *)dest;
  size_t in_left = srcsize;
  size_t out_left = destsize;

  size_t r = iconv(iw.t, &pin, &in_left, &pout, &out_left);

  if(r == (size_t)-1 && errno != E2BIG)
    throw conversion_failed();
  return make_pair(pin - (char *)src, pout - (char *)dest);
}

# endif /* defined USE_UTF8_STRING */

bool take_text(connection &con, Window w, Atom prop, ostream &out)
{
  Atom type;
  int format;
  unsigned long nitems;
  auto_xptr<uchar> data = get_whole_property(con, w, prop, AnyPropertyType, &type, &format, &nitems);
  if(format != 8)
    return false;
  if(type == XA_STRING || type == con.intern("COMPOUND_TEXT"))
    decode_ctext(con, data.get(), nitems, out);
# ifdef USE_UTF8_STRING
  else if(type == con.intern("UTF8_STRING"))
    decode_utf8(data.get(), nitems, out);
# endif
  else
    return false;
  return true;
}

void read_partial_text(connection &con, Window w, Atom prop, buf_type &out)
{
  Atom type;
  int format;
  unsigned long nitems;
  auto_xptr<uchar> value = get_whole_property(con, w, prop, AnyPropertyType, &type, &format, &nitems);
  TRC << "read_partial_text: type=" << a2s(con, type) << "; nitems=" << nitems << "; format=" << format << "\n";
  if(type != XA_STRING && type != con.intern("COMPOUND_TEXT")
# ifdef USE_UTF8_STRING
      && type != con.intern("UTF8_STRING")
# endif
      || format != 8)
    throw error("unknown type");
  out.insert(out.end(), value.get(), value.get() + nitems);
}

void discard_incr(connection &con, listener &lis, Window w, Atom prop)
{
  XDeleteProperty(con.dpy(), w, prop);
  while(true)
  {
    wait_for_prop(lis, w, prop, 10000);
    if(!property_length_z(con, w, prop))
      break;
    XDeleteProperty(con.dpy(), w, prop);
  }
}

void ctext_incr(connection &con, listener &lis, Window w, Atom prop, ostream &out)
{
  buf_type buf;
  while(property_length_z(con, w, prop))
  {
    try {
      read_partial_text(con, w, prop, buf);
      XDeleteProperty(con.dpy(), w, prop);
    }
    catch(...)
    {
      discard_incr(con, lis, w, prop);
      throw;
    }
    wait_for_prop(lis, w, prop, 10000);
  }
  decode_ctext(con, (unsigned char *)&buf[0], buf.size(), out);
}

# ifdef USE_UTF8_STRING

void utf8_incr(connection &con, listener &lis, Window w, Atom prop, ostream &out)
{
  buf_type buf;
  iconv_w iw(nl_langinfo(CODESET), "UTF-8");
  while(property_length_z(con, w, prop))
  {
    try {
      read_partial_text(con, w, prop, buf);
      int consumed = decode_utf8_partial(iw, (unsigned char *)&buf[0], buf.size(), out);
      buf.erase(buf.begin(), buf.begin() + consumed);
      XDeleteProperty(con.dpy(), w, prop);
    }
    catch(...)
    {
      discard_incr(con, lis, w, prop);
      throw;
    }
    wait_for_prop(lis, w, prop, 10000);
  }
}

# endif /* defined USE_UTF8_STRING */

void read_incr(connection &con, listener &lis, Window w, Atom prop, ostream &out)
{
  wait_for_prop(lis, w, prop, 10000);
  Atom type = property_type(con, w, prop);
  if(type == XA_STRING || type == con.intern("COMPOUND_TEXT"))
    ctext_incr(con, lis, w, prop, out);
# ifdef USE_UTF8_STRING
  else if(type == con.intern("UTF8_STRING"))
    utf8_incr(con, lis, w, prop, out);
# endif
  else
  {
    discard_incr(con, lis, w, prop);
    ERROR("unknown type: " << a2s(con, type))
  }
}

bool convert_selection(connection &con, listener &lis, Window win, Atom selection, Atom prop, Atom target)
{
  TRC << "convert selection: selection=" << a2s(con, selection) << "; target=" << a2s(con, target) << "; property=" << a2s(con, prop) << "\n";
  XDeleteProperty(con.dpy(), win, prop);
  XConvertSelection(con.dpy(), selection, target, prop, win, lis.current_time());
  return wait_for_selection(lis, 10000).property != None;
}

set<Atom> default_targets(connection &con)
{
  set<Atom> ret;
  ret.insert(con.intern("COMPOUND_TEXT"));
  ret.insert(XA_STRING);
  return ret;
}

set<Atom> get_targets(connection &con, listener &lis, Window win, Atom selection)
{
  if(!convert_selection(con, lis, win, selection, con.intern("TEXT"), con.intern("TARGETS")))
    return default_targets(con);
  Atom type;
  int format;
  unsigned long nitems;
  auto_xptr<Atom> targets = get_whole_property(con, win, con.intern("TEXT"), XA_ATOM, &type, &format, &nitems).cast<Atom>();
  if(format != 32)
    return default_targets(con);
  return set<Atom>(targets.get(), targets.get() + nitems);
}

bool drain_with(connection &con, listener &lis, Window win, Atom selection, Atom prop, Atom target, ostream &out)
{
  if(!convert_selection(con, lis, win, selection, prop, target))
  {
    TRC << "convert_selection failed\n";
    return false;
  }
  if(take_incr(con, win, prop))
    read_incr(con, lis, win, prop, out);
  else if(take_text(con, win, prop, out))
    ;
  else
  {
    XDeleteProperty(con.dpy(), win, prop);
    return false;
  }
  return true;
}

void from_clip(const char *dpyname, const string &selection, ostream &out)
{
  connection con(dpyname);
  Window win = XCreateSimpleWindow(con.dpy(), DefaultRootWindow(con.dpy()), 0, 0, 1, 1, 0, 0, 0);
  select_input(con, win, PropertyChangeMask);

  listener lis(con, win);

  const Atom sel = con.intern(selection);

  set<Atom> targets = get_targets(con, lis, win, sel);

# ifdef TRACE
  TRC << "targets = ";
  for(set<Atom>::iterator it = targets.begin(); it != targets.end(); ++it)
    TRC << a2s(con, *it) << " ";
  TRC << "\n";
# endif

  const char *attempts[] = {
# ifdef USE_UTF8_STRING
    "UTF8_STRING", 
# endif
    "COMPOUND_TEXT", "STRING"};
  for(int i = 0; i < sizeof attempts / sizeof *attempts; i++)
    if(targets.find(con.intern(attempts[i])) != targets.end() &&
        drain_with(con, lis, win, sel, con.intern("TEXT"), con.intern(attempts[i]), out))
      return;
  TRC << "no data can be copied\n";
}

int max_content_size(connection &con)
{
# ifdef TRACE
  return 4;
# else
  return XMaxRequestSize(con.dpy()) * 4 - 1000; /* 1000 is an estimated upper bound of protocol overhead */
# endif
}

void notify_selection(connection &con, const XSelectionRequestEvent &sre, Atom prop)
{
  XEvent to_send;
  to_send.type = SelectionNotify;
  XSelectionEvent &se = to_send.xselection;
  se.requestor = sre.requestor;
  se.selection = sre.selection;
  se.target = sre.target;
  se.property = prop;
  se.time = sre.time;

  error_checker chk(con);
  XSendEvent(con.dpy(), sre.requestor, False, NoEventMask, &to_send);
  check_errors(chk);
}

void put_text(connection &con, Window requestor, Atom prop, Atom type, unsigned char *data, int size)
{
  TRC << "put_text size=" << size << "\n";
  set_property(con, requestor, prop, type, 8, data, size);
}

# ifdef USE_UTF8_STRING

int put_in_utf8(connection &con, Window w, Atom prop, const char *src, int size)
{
  iconv_w iw("UTF-8", nl_langinfo(CODESET));
  int bufsize = max_content_size(con);
  buf_type buf(bufsize);
  pair<int, int> consumed_size = encode_utf8_partial(iw, src, size, &buf[0], bufsize);
  put_text(con, w, prop, con.intern("UTF8_STRING"), &buf[0], consumed_size.second);
  return consumed_size.first;
}

# endif /* defined USE_UTF8_STRING */

Atom request_prop(const XSelectionRequestEvent &sre)
{
  return sre.property != None ? sre.property : sre.target;
}

enum string_format
{
  x_compound_text,
# ifdef USE_UTF8_STRING
  x_utf8_string,
# endif
  x_string
};

XICCEncodingStyle format_style(string_format fmt)
{
  switch(fmt)
  {
  case x_string:
    return XStringStyle;
  case x_compound_text:
    return XCompoundTextStyle;
# ifdef USE_UTF8_STRING
  case x_utf8_string:
    throw error("format_style(x_utf8_string)");
# endif
  }
}

Atom format_type(connection &con, string_format fmt)
{
  switch(fmt)
  {
  case x_string:
    return XA_STRING;
  case x_compound_text:
    return con.intern("COMPOUND_TEXT");
# ifdef USE_UTF8_STRING
  case x_utf8_string:
    return con.intern("UTF8_STRING");
# endif
  }
}

auto_ptr<buf_type> encode(connection &con, string_format fmt, const string &data)
{
  XTextProperty tp;
  char *list[2];
  list[0] = (char *)data.c_str();
  list[1] = NULL;
  int r = XmbTextListToTextProperty(con.dpy(), list, 1, format_style(fmt), &tp);
  if(r == XNoMemory)
    throw error("no memory");
  else if(r == XLocaleNotSupported)
    throw error("locale not supported");
  const auto_xptr<uchar> guard(tp.value);
  if(tp.format != 8 || tp.encoding != format_type(con, fmt))
    throw error("unexpected behaviour of XmbTextListToTextProperty");
  if(r != Success)
    throw conversion_failed();
  return auto_ptr<buf_type>(new buf_type(tp.value, tp.value + tp.nitems));
}

struct incr_session
{
  incr_session(time_t i, string_format t) : offset(0), initiated(i), fmt(t) {}
  int offset;
  const time_t initiated;
  const string_format fmt;
};

typedef pair<Window, Atom> location;

struct selection_server
{
  selection_server(connection &c, const string *d, Time o);
  void on_selection_request(const XSelectionRequestEvent &sre);
  void on_destroy(const XDestroyWindowEvent &dwe);
  void on_property_change(const XPropertyEvent &pe);
  bool serving() const { return !sessions.empty(); }
  void tick(time_t now);
private:
  /* NONCOPYABLE */
  selection_server(const selection_server &);
  selection_server &operator=(const selection_server &);

  typedef map<location, incr_session> sessions_type;

  bool handle_selection_request(const XSelectionRequestEvent &sre);
  bool send_text(Window requestor, Atom prop, string_format fmt);
  bool send_targets(Window requestor, Atom prop);
  bool handle_multiple(const XSelectionRequestEvent &sre);
  bool send_timestamp(Window requestor, Atom prop);
  bool initiate_incr(Window requestor, Atom prop, string_format fmt);
  void step_incr(sessions_type::iterator ps);
  void remove_session(sessions_type::iterator ps);

  connection &con;
  const Time owned;
  sessions_type sessions;
  bool representable_in_x_string;
  auto_ptr<buf_type> ctext;
  const string *data;
};

selection_server::selection_server(connection &c, const string *d, Time o)
  : con(c), owned(o), data(d)
{
  try
  {
    ctext = encode(con, x_string, *d);
    TRC << "representable\n";
    representable_in_x_string = true;
  }
  catch(conversion_failed)
  {
    ctext = encode(con, x_compound_text, *d);
    TRC << "unrepresentable\n";
    representable_in_x_string = false;
  }
}

void selection_server::on_selection_request(const XSelectionRequestEvent &sre)
{
  TRC << "request: target=" << a2s(con, sre.target) << "; property=" << a2s(con, sre.property) << "\n";
  try
  {
    notify_selection(con, sre,
        handle_selection_request(sre) ? request_prop(sre) : None);
  }
  catch(bad_window)
  {
    TRC << "ignoring bad_window\n";
  }
}

void selection_server::on_destroy(const XDestroyWindowEvent &dwe)
{
  TRC << "on_destroy\n";
  vector<sessions_type::iterator> to_remove;
  for(sessions_type::iterator it = sessions.begin(); it != sessions.end(); ++it)
    if(it->first.first == dwe.window)
      to_remove.push_back(it);
  for_each(to_remove.begin(), to_remove.end(), bind1st(mem_fun(&selection_server::remove_session), this));
}

void selection_server::on_property_change(const XPropertyEvent &pe)
{
  if(pe.state != PropertyDelete)
    return;
  sessions_type::iterator it = sessions.find(location(pe.window, pe.atom));
  if(it != sessions.end())
    step_incr(it);
}

void selection_server::tick(time_t now)
{
  const int incr_timeout = 60;

  vector<sessions_type::iterator> to_remove;
  for(sessions_type::iterator it = sessions.begin(); it != sessions.end(); ++it)
    if(now - it->second.initiated > incr_timeout)
      to_remove.push_back(it);
  for_each(to_remove.begin(), to_remove.end(), bind1st(mem_fun(&selection_server::remove_session), this));
}

bool selection_server::handle_selection_request(const XSelectionRequestEvent &sre) try
{
  Window rq = sre.requestor;
  Atom prop = request_prop(sre);
  TRC << "sre.time=" << sre.time << "; owned=" << owned << "\n";
  if(sre.time != CurrentTime && sre.time < owned)
    return false;
  else if(sre.target == con.intern("COMPOUND_TEXT") || sre.target == con.intern("TEXT"))
    return send_text(rq, prop, x_compound_text);
# ifdef USE_UTF8_STRING
  else if(sre.target == con.intern("UTF8_STRING"))
    return send_text(rq, prop, x_utf8_string);
# endif
  else if(sre.target == XA_STRING)
    return send_text(rq, prop, x_string);
  else if(sre.target == con.intern("TARGETS"))
    return send_targets(rq, prop);
  else if(sre.target == con.intern("MULTIPLE"))
    return handle_multiple(sre);
  else if(sre.target == con.intern("TIMESTAMP"))
    return send_timestamp(rq, prop);
  else
  {
    TRC << "unknown target\n";
    return false;
  }
}catch(x_bad_alloc)
{
  TRC << "BadAlloc detected: refusing request\n";
  return false;
}catch(conversion_failed)
{
  TRC << "conversion failed: refusing request\n";
  return false;
}

bool selection_server::send_text(Window requestor, Atom prop, string_format fmt)
{
  TRC << "send_text\n";
  if(fmt == x_string && !representable_in_x_string)
    return false;
# ifdef USE_UTF8_STRING
  if(fmt == x_utf8_string)
    if(data->size() <= max_content_size(con) / 4)
    {
      put_in_utf8(con, requestor, prop, data->c_str(), data->size());
      return true;
    }
    else
      return initiate_incr(requestor, prop, fmt);
  else
# endif
    if(ctext->size() <= max_content_size(con))
    {
      put_text(con, requestor, prop, format_type(con, fmt), &(*ctext)[0], ctext->size());
      return true;
    }
    else
      return initiate_incr(requestor, prop, fmt);
}

bool selection_server::send_targets(Window requestor, Atom prop)
{
  vector<Atom> targets;
  targets.push_back(con.intern("TARGETS"));
  targets.push_back(con.intern("MULTIPLE"));
  targets.push_back(con.intern("TIMESTAMP"));
  targets.push_back(con.intern("COMPOUND_TEXT"));
# ifdef USE_UTF8_STRING
  targets.push_back(con.intern("UTF8_STRING"));
# endif
  targets.push_back(con.intern("TEXT"));
  if(representable_in_x_string)
    targets.push_back(XA_STRING);
  set_property(con, requestor, prop, XA_ATOM, 32, (unsigned char *)&targets[0], targets.size());
  return true;
}

bool selection_server::handle_multiple(const XSelectionRequestEvent &sre)
{
  if(sre.property == None)
    return false;
  try
  {
    Atom type;
    unsigned long nitems;
    int format;
    auto_xptr<Atom> value = 
      get_whole_property(con, sre.requestor, sre.property, con.intern("ATOM_PAIR"), &type, &format, &nitems).cast<Atom>();
    if(format != 32 || nitems % 2 != 0)
      return false;
    for(int i = 0; i < nitems; i += 2)
      if(value[i+1] != None)
      {
        XSelectionRequestEvent subreq = sre;
        subreq.target = value[i];
        subreq.property = value[i+1];
        if(!handle_selection_request(subreq))
          value[i+1] = None;
      }
    set_property(con, sre.requestor, sre.property, con.intern("ATOM_PAIR"), 32, (unsigned char *)value.get(), nitems);
    return true;
  }
  catch(no_such_property)
  {
    return false;
  }
}

bool selection_server::send_timestamp(Window requestor, Atom prop)
{
  set_property(con, requestor, prop, XA_INTEGER, 32, (unsigned char *)&owned, 1);
  return true;
}

bool selection_server::initiate_incr(Window requestor, Atom prop, string_format fmt)
{
  TRC << "initiate_incr\n";
  location loc(requestor, prop);
  if(sessions.find(loc) != sessions.end())
    return false;
  long size = ctext->size();
  select_input(con, requestor, PropertyChangeMask | StructureNotifyMask); /* FIXME: dirty hack! */
  set_property(con, requestor, prop, con.intern("INCR"), 32, (unsigned char *)&size, 1);
  sessions.insert(make_pair(loc, incr_session(time(NULL), fmt)));
  return true;
}

void selection_server::step_incr(sessions_type::iterator ps)
{
  try
  {
    const location &loc = ps->first;
    incr_session &ss = ps->second;
# ifdef USE_UTF8_STRING
    if(ss.fmt == x_utf8_string)
    {
      int consumed = put_in_utf8(con, loc.first, loc.second, data->c_str() + ss.offset, data->size() - ss.offset);
      ss.offset += consumed;
      if(!consumed)
        remove_session(ps);
    }
    else
# endif
    {
      int len = min((int)ctext->size() - ss.offset, max_content_size(con));
      TRC << "step_incr: len=" << len << "\n";
      put_text(con, loc.first, loc.second, format_type(con, ss.fmt), (unsigned char *)&(*ctext)[ss.offset], len);
      ss.offset += len;
      if(!len)
        remove_session(ps);
    }
  }
  catch(exception &e)
  {
    TRC << "exception during step_incr: " << e.what() << "\n";
    remove_session(ps);
  }
}

void selection_server::remove_session(sessions_type::iterator ps)
{
  TRC << "removing session\n";
  sessions.erase(ps);
}

void to_clip(const char *dpyname, const string &selection, const string &data)
{
  connection con(dpyname);
  Window win = XCreateSimpleWindow(con.dpy(), DefaultRootWindow(con.dpy()), 0, 0, 1, 1, 0, 0, 0);
  select_input(con, win, PropertyChangeMask);

  listener lis(con, win);

  Atom sel_a = con.intern(selection);
  Time owned = lis.current_time();
  XSetSelectionOwner(con.dpy(), sel_a, win, owned);

  time_t last_tick = time(NULL);
  selection_server ss(con, &data, owned);
  while(win == XGetSelectionOwner(con.dpy(), sel_a) || ss.serving())
  {
    try
    {
      XEvent ev = lis.next_with_timeout(3000);
      switch(ev.type)
      {
      case SelectionRequest:
        ss.on_selection_request(ev.xselectionrequest);
        break;
      case DestroyNotify:
        ss.on_destroy(ev.xdestroywindow);
        break;
      case PropertyNotify:
        ss.on_property_change(ev.xproperty);
        break;
      default:
        TRC << "unknown event\n";
      }
    }
    catch(wait_timeout) {}
    time_t now = time(NULL);
    if(now != last_tick)
    {
      ss.tick(now);
      last_tick = now;
    }
  }
}

struct config
{
  enum operation_mode
  {
    m_read,
    m_write,
    m_help,
    m_version,
    m_unspecified
  };

  config() : mode(m_unspecified), selection("CLIPBOARD") {}

  operation_mode mode;
  vector<string> files;
  string selection;
  string display;
};

struct option
{
  /* implicit constructors */
  option(char c) : is_short(true), val(1, c) {}
  option(const string &s) : is_short(false), val(s) {}
  option(const char *s) : is_short(false), val(s) {}

  bool operator<(const option &other) const
  {
    return is_short < other.is_short || is_short == other.is_short && val < other.val;
  }
private:
  friend ostream &operator<<(ostream &ost, const option &opt)
  {
    return ost << (opt.is_short ? "-" : "--") << opt.val;
  }
  bool is_short;
  string val;
};

typedef void (*nullary)(config &);
typedef void (*unary)(config &, const string &);
typedef map<option, nullary> nullaries_type;
typedef map<option, unary> unaries_type;

# define NO_SUCH_OPTION(opt) ERROR("no such option: " << ::option(opt))
# define ARGUMENT_REQUIRED(opt) ERROR("argument required: " << ::option(opt))

void parse_general(config &cfg, char **argv, const nullaries_type &nullaries, const unaries_type &unaries, unary bare_arg)
{
  for(int i = 0; argv[i]; i++)
  {
    const char *s = argv[i];
    if(s[0] == '-' && s[1] == '-')
    {
      if(const char *eq = strchr(s, '='))
      {
        string name(s + 2, eq);
        unaries_type::const_iterator it = unaries.find(name);
        if(it == unaries.end())
          NO_SUCH_OPTION(name)
        it->second(cfg, eq + 1);
      }
      else
      {
        string name(s + 2);
        unaries_type::const_iterator it = unaries.find(name);
        if(it != unaries.end())
        {
          if(!argv[++i])
            ARGUMENT_REQUIRED(name)
          it->second(cfg, argv[i]);
        }
        else
        {
          nullaries_type::const_iterator nit = nullaries.find(name);
          if(nit == nullaries.end())
            NO_SUCH_OPTION(name)
          nit->second(cfg);
        }
      }
    }
    else if(s[0] == '-' && s[1])
    {
      for(int j = 1; s[j]; j++)
      {
        char c = s[j];
        unaries_type::const_iterator it = unaries.find(c);
        if(it != unaries.end())
        {
          string arg;
          if(s[j+1])
            arg = s + j + 1;
          else if(argv[++i])
            arg = argv[i];
          else ARGUMENT_REQUIRED(c)
          it->second(cfg, arg);
          break;
        }
        else
        {
          nullaries_type::const_iterator nit = nullaries.find(c);
          if(nit == nullaries.end())
            NO_SUCH_OPTION(c)
          nit->second(cfg);
        }
      }
    }
    else
      bare_arg(cfg, s);
  }
}

void opt_read(config &cfg) { cfg.mode = config::m_read; }
void opt_write(config &cfg) { cfg.mode = config::m_write; }
void opt_help(config &cfg) { cfg.mode = config::m_help; }
void opt_version(config &cfg) { cfg.mode = config::m_version; }
void opt_primary(config &cfg) { cfg.selection = "PRIMARY"; }
void opt_clipboard(config &cfg) { cfg.selection = "CLIPBOARD"; }
void opt_selection(config &cfg, const string &sel) { cfg.selection = sel; }
void opt_display(config &cfg, const string &name) { cfg.display = name; }
void opt_fname(config &cfg, const string &name) { cfg.files.push_back(name); }

config parse_options(char **argv)
{
  config cfg;
  nullaries_type nullaries;
  unaries_type unaries;

  nullaries["read"] = nullaries['r'] = opt_read;
  nullaries["write"] = nullaries['w'] = opt_write;
  nullaries["help"] = nullaries['h'] = opt_help;
  nullaries["version"] = opt_version;
  nullaries['p'] = opt_primary;
  nullaries['c'] = opt_clipboard;

  unaries["selection"] = unaries['s'] = opt_selection;
  unaries["display"] = opt_display;

  parse_general(cfg, argv, nullaries, unaries, opt_fname);
  return cfg;
}

void show_help()
{
  const char *help_string =
    "Usage: mclip [MODE] [OPTION]... [FILE]...\n"
    "Copy data to and from the X clipboard.\n"
    "\n"
    "When none of the modes is specified, operate in write mode if\n"
    "FILE is given, in read mode otherwise.\n"
    "\n"
    "Modes:\n"
    "  -r, --read                 read from the X clipboard.\n"
    "  -w, --write                write to the X clipboard.\n"
    "  -h, --help                 print this usage information.\n"
    "      --version              print version information.\n"
    "\n"
    "Options:\n"
    "  -c                         (default) same as --selection=CLIPBOARD.\n"
    "      --display=DISPLAY      use X display DISPLAY.\n"
    "  -p                         same as --selection=PRIMARY.\n"
    "  -s, --selection=SELECTION  use X selection SELECTION.\n"
    "\n"
    "Report bugs to <m@kotha.net>\n";
  cout << help_string;
}

void show_version()
{
  cout << APP_NAME << " " << APP_VERSION << "\n";
}

const char *dpyname(const config &cfg)
{
  return cfg.display.empty() ? NULL : cfg.display.c_str();
}

void do_read(const config &cfg)
{
  from_clip(dpyname(cfg), cfg.selection, cout);
}

void read_files(const vector<string> &files, ostream &out)
{
  if(files.empty())
    out << cin.rdbuf();
  else
    for(int i = 0; i < files.size(); i++)
    {
      string name = files[i];
      if(name == "-")
        out << cin.rdbuf();
      else
      {
        ifstream ifs(files[i].c_str());
        if(!ifs)
          ERROR("couldn't open " << files[i])
        out << ifs.rdbuf();
      }
    }
}

void go_background()
{
  switch(fork())
  {
  case -1:
    throw error("fork failed");
  case 0:
    g_forked = true;
    break;
  default:
    _exit(0);
  }
}

void do_write(const config &cfg)
{
  ostringstream ost;
  read_files(cfg.files, ost);
# ifndef TRACE
  go_background();
# endif
  to_clip(dpyname(cfg), cfg.selection, ost.str());
}

void report(const string &msg)
{
  if(!g_forked)
  {
    cerr << APP_NAME << ": " << msg << "\n";
  }
}

config::operation_mode real_mode(const config &cfg)
{
  if(cfg.mode != config::m_unspecified)
    return cfg.mode;
  return cfg.files.empty() ? config::m_read : config::m_write;
}

int main(int ac, char **av) try
{
  setlocale(LC_ALL, "");
# ifdef PACKAGE_VERSION
  if(strcmp(APP_VERSION, PACKAGE_VERSION))
    throw error("version mismatch");
# endif
  config cfg = parse_options(av + 1);
  switch(real_mode(cfg))
  {
  case config::m_help:
    show_help();
    break;
  case config::m_version:
    show_version();
    break;
  case config::m_read:
    do_read(cfg);
    break;
  case config::m_write:
    do_write(cfg);
    break;
  }
}
catch(exception &e)
{
  report(e.what());
  return 1;
}
catch(...)
{
  report("unknown error");
  return 1;
}
