#!/usr/bin/python3
#
# Copyright (C) 2014 Dragon.Studio
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# For a copy of the License see <http://www.gnu.org/licenses/gpl-3.0.html>
#
# Command to install conwin dependencies in Debian-compatible distro:
# sudo apt-get install python-dill python-xlib xdotool; sudo pip install ewmh

from array import *
from os import system
from time import sleep
from ewmh import EWMH
from Xlib import display, protocol, X
from Xlib.protocol.request import *
from Xlib.protocol.event import *
import Xlib
import argparse
import dill
from copy import copy, deepcopy

ewmh = EWMH()
disp = display.Display()
poll_interval = 0.025 # s
poll_attempts_limit = 10

class Geometry:
  def __init__(self, desktop, y, x, height, width):
    self.desktop = desktop
    self.y       = y
    self.x       = x
    self.height  = height
    self.width   = width

class Window:
  def __init__(self, window, id, name, geometry, state, xwin):
    self.window   = window
    self.id       = id
    self.name     = name
    self.geometry = geometry
    self.state    = state
    self.xwin     = xwin

def border(frame, client):
  border = client
  while(border.query_tree().parent != frame):
    border = border.query_tree().parent
    return border

def frame(client):
  frame = client
  while(frame.query_tree().parent != ewmh.root):
    frame = frame.query_tree().parent
  return frame

def list_windows():
  windows = ewmh.getClientList()
  win = []
  for client in windows:
    try:
      geo = frame(client).get_geometry()
    except:
      continue
    b = border(frame(client), client).get_geometry()
#    print("Name: ", ewmh.getWmName(client))
#    print("Geometry: ", geo.x, "x", geo.y, "+", geo.width, "+", geo.height)
    geometry = Geometry(ewmh.getWmDesktop(client), geo.y, geo.x, b.height, b.width)
    win.append(Window(window   = client,
                      id       = client.id,
                      name     = ewmh.getWmName(client),
                      geometry = geometry,
                      state    = ewmh.getWmState(client, True),
                      xwin     = disp.create_resource_object("window", client.id)))
  return win

def switch_to_desktop(desktop):
  ewmh.setCurrentDesktop(desktop)
  ewmh.display.flush()

def load(config):
  old_win = dill.load(config)
  all_win = list_windows()

  # For each window it is necessary to unmaximize, switch to window's desktop, unmap and map it so we can configure its geometry programatically
  current_desktop = ewmh.getCurrentDesktop()
  for client in all_win:
    unmaximize(client.window)
  ewmh.display.flush()

  for client in all_win:
    switch_to_desktop(client.geometry.desktop)
#    system("xdotool windowunmap --sync " + str(client.window.id))
    client.xwin.unmap()
  poll_attempts = 0
  for client in all_win:
    while client.xwin.get_attributes().map_state == X.IsViewable and poll_attempts < poll_attempts_limit:
      sleep(poll_interval)
      poll_attempts += 1

  for client in all_win:
    switch_to_desktop(client.geometry.desktop)
#    system("xdotool windowmap   --sync " + str(client.window.id))
    client.xwin.map()
  poll_attempts = 0
  for client in all_win:
    while client.xwin.get_attributes().map_state != X.IsViewable \
      and poll_attempts < poll_attempts_limit:
      sleep(poll_interval)
      poll_attempts += 1

  for client in all_win:
    old_client = [existing for existing in old_win if client.id == existing.id]
    old_client[0].window = client.window
    if old_client:
      client = old_client[0]
    else:
      old_client = [existing for existing in old_win if client.name == existing.name and
                                                        client.name != None]
      if old_client:
        old_client[0].window = client.window
        client = old_client[0]
    switch_to_desktop(client.geometry.desktop)
    client.window.configure(y      = client.geometry.y,
                            x      = client.geometry.x,
                            height = client.geometry.height,
                            width  = client.geometry.width)
    ewmh.setWmDesktop(client.window, client.geometry.desktop)
    set_state(client.window, client.state)
  ewmh.display.flush()
  switch_to_desktop(current_desktop)

class SavedWindow:
  def __init__(self, id, name, geometry, state, window):
    self.id = id
    self.name = name
    self.geometry = geometry
    self.state = {}
    self.window = window

def save(config):
  windows = list_windows()
  saved_windows = []
  for client in windows:
    client_state = {}
    for state in EWMH.NET_WM_STATES:
      client_state[state] = client.state
    saved_windows.append(SavedWindow(client.id, client.name, client.geometry, client.state, None))
  dill.dump(saved_windows, config)

def unmaximize(window):
  ewmh.setWmState(window, 0, "_NET_WM_STATE_MAXIMIZED_VERT")
  ewmh.setWmState(window, 0, "_NET_WM_STATE_MAXIMIZED_HORZ")
  ewmh.setWmState(window, 0, "_NET_WM_STATE_FULLSCREEN")

def set_state(window, states):
  for possible_state in EWMH.NET_WM_STATES:
    if possible_state not in states:
      ewmh.setWmState(window, 0, possible_state)
  for state in states:
    ewmh.setWmState(window, 1, state)

parser = argparse.ArgumentParser(
           description = "Save or load configuration (geometry and state) of all windows")
group = parser.add_mutually_exclusive_group(required = True)
group.add_argument("--load", "-l", metavar = "$FILE",
                                   type    = argparse.FileType("rb"),
                                   help    = "Load configuration from specified file")
group.add_argument("--save", "-s", metavar = "$FILE",
                                   type    = argparse.FileType("wb"),
                                   help    = "Save configuration from specified file")
args = parser.parse_args()
if args.save: save(args.save)
if args.load: load(args.load)
