#include <ctime>
#include <vector>
#include <array>
#include <signal.h>
#include <unistd.h>
#include <fstream>
#include <gtk/gtk.h>
#include <X11/Xlib.h>
#include <libwebsockets.h>

#include "../zdconf/zdbase.h"
#include "../zdconf/zddesktop.h"
#include "../zdconf/zserver.h"


ZDDesktop zddesktop;
ZServer zserver;

////////////////////////////build window////////////////////////////

GtkWidget *window;
GtkCssProvider *provider;
GtkWidget *bgfixed;
GtkWidget *bgimage;
std::vector<std::array<GtkWidget*, 2>> shortcuts;
GtkWidget *robotimage;

const gchar *CSS_DATA = ".scl{color:white;font-weight:bold} .bg{background:#D55500} ";
const std::string TEMP_PID_DIR = std::string(getenv("HOME")) + "/.zddeskpid";
const int WINDOW_PADDING = 30;
const int SHORTCUT_MIN_XPADDING = 20;
const int SHORTCUT_MIN_YPADDING = 20;
const int SHORTCUT_ICON_IMG_SIZE = 48;
const int SHORTCUT_ICON_SIZE = 60;
const int SHORTCUT_ICON_XPENDING = 21;
const int SHORTCUT_LABEL_WIDTH = 100;
const int SHORTCUT_LABEL_HEIGHT = 25;
const int SHORTCUT_LABEL_TOPPENDING = 60;
const int SHORTCUT_WIDTH = SHORTCUT_LABEL_WIDTH;
const int SHORTCUT_HEIGHT = SHORTCUT_LABEL_TOPPENDING+SHORTCUT_LABEL_HEIGHT;
const int ROBOT_DISPLAY_WIDTH = 180;
const int ROBOT_DISPLAY_HEIGHT = 180;
const std::string ROBOT_OPEN_APP_PREFIX = "打开";
const std::string ROBOT_OPEN_APP_SUCCESS = "好的，已启动应用";
const std::string ROBOT_OPEN_APP_FAIL = "没有找到这个应用";
const std::string ROBOT_WRITE_SUCCESS = "好的，已写入";


void addClassStyle(GtkWidget *widget, const gchar *classname){
    GtkStyleContext *context = gtk_widget_get_style_context(widget);
    gtk_style_context_add_class(context, classname);
}

gboolean bypassInput(gpointer user_data){
    GdkWindow* gdkwindow = gtk_widget_get_window(window);
    
    cairo_region_t *mask_region;
    size_t sccount = shortcuts.size();
    if(sccount==0){
        mask_region = cairo_region_create();
    }
    else{
        cairo_rectangle_int_t rectangles[sccount];
        for(size_t i=0; i<sccount;i++){
            gtk_widget_get_allocation(shortcuts[i][0], &rectangles[i]);
            rectangles[i].height += 60;   //for the label under the icon
        }
        mask_region=cairo_region_create_rectangles(rectangles, sccount);
    }
    gdk_window_input_shape_combine_region(gdkwindow, mask_region, 0, 0);
    cairo_region_destroy(mask_region);
    
    return G_SOURCE_REMOVE;
}

void scbutton_Press(GtkWidget *widget, GdkEventButton *event, gpointer data){
    if((event->button==1)&&(event->type==GDK_2BUTTON_PRESS)){
        GtkLabel *cmdlabel = (GtkLabel *)data;
        const gchar *cmd = gtk_label_get_label(cmdlabel);
        ZDBase::Exec(cmd);
    }
}

/*
void change_CursorPointer(GtkWidget *widget, gpointer data){
    GdkCursor *cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "pointer");
    gdk_window_set_cursor(gdk_get_default_root_window(), cursor);
}

void change_CursorDefault(GtkWidget *widget, gpointer data){
    GdkCursor *cursor = gdk_cursor_new_from_name(gdk_display_get_default(), "default");
    gdk_window_set_cursor(gdk_get_default_root_window(), cursor);
}
*/

void fillShortCuts(const GdkRectangle &rect){
    int livewidth = rect.width-2*WINDOW_PADDING;
    int liveheight = rect.height-2*WINDOW_PADDING;
    int xcount = (livewidth-SHORTCUT_MIN_XPADDING)/(SHORTCUT_WIDTH+SHORTCUT_MIN_XPADDING);
    int xpadding = (livewidth-xcount*SHORTCUT_WIDTH)/(xcount+1);
    int curx = xpadding+WINDOW_PADDING;
    int xstep = SHORTCUT_WIDTH+xpadding;
    int ycount = (liveheight-SHORTCUT_MIN_YPADDING)/(SHORTCUT_WIDTH+SHORTCUT_MIN_YPADDING);
    int ypadding = (liveheight-ycount*SHORTCUT_HEIGHT)/(ycount+1);
    int cury = ypadding+WINDOW_PADDING;
    int ystep = SHORTCUT_HEIGHT+ypadding;
    
    int xpos=0, ypos=0;
    std::vector<ShortCut> scs = zddesktop.GetShortCuts();
    for(size_t i=0; i<scs.size();i++){
        // build hide command label
        std::string cmd;
        if(scs[i].Type=="web"){
            cmd = "firefox --new-tab '" + scs[i].Link + "' >/dev/null 2>&1 &";
        }
        else{
            cmd = scs[i].Link + " >/dev/null 2>&1 &";
        }
        GtkWidget *cmdlabel = gtk_label_new(cmd.c_str());
        
        // build icon button with icon image
        GtkWidget *scbutton = gtk_button_new();
        GdkPixbuf *scpixbuf;
        GError* err = NULL;
        if(scs[i].Icon.empty()){
            scpixbuf = gdk_pixbuf_new_from_file_at_size(zddesktop.DefaultIconDir.c_str(), SHORTCUT_ICON_IMG_SIZE, SHORTCUT_ICON_IMG_SIZE, &err);
            if(err!=NULL) return;
        }
        else {
            scpixbuf = gdk_pixbuf_new_from_file_at_size((zddesktop.IconPath+scs[i].Icon).c_str(), SHORTCUT_ICON_IMG_SIZE, SHORTCUT_ICON_IMG_SIZE, &err);
            if(err!=NULL){
                err = NULL;
                scpixbuf = gdk_pixbuf_new_from_file_at_size(zddesktop.DefaultIconDir.c_str(), SHORTCUT_ICON_IMG_SIZE, SHORTCUT_ICON_IMG_SIZE, &err);
                if(err!=NULL) return;
            }
        }
        GtkWidget *scimage = gtk_image_new_from_pixbuf(scpixbuf);
        gtk_button_set_image(GTK_BUTTON(scbutton), scimage);
        gtk_widget_set_size_request(scbutton, SHORTCUT_ICON_SIZE, SHORTCUT_ICON_SIZE);
        g_signal_connect(G_OBJECT(scbutton), "button-press-event", G_CALLBACK(scbutton_Press), cmdlabel);
        //g_signal_connect(G_OBJECT(scbutton), "enter-notify-event", G_CALLBACK(change_CursorPointer), cmdlabel);
        //g_signal_connect(G_OBJECT(scbutton), "leave-notify-event", G_CALLBACK(change_CursorDefault), cmdlabel);
        gtk_fixed_put(GTK_FIXED(bgfixed), scbutton, curx+SHORTCUT_ICON_XPENDING, cury);
        
        // build icon label
        GtkWidget *sclabel = gtk_label_new(scs[i].Name.c_str());
        gtk_widget_set_size_request(sclabel, SHORTCUT_LABEL_WIDTH, SHORTCUT_LABEL_HEIGHT);
        gtk_label_set_ellipsize(GTK_LABEL(sclabel), PANGO_ELLIPSIZE_END);
        addClassStyle(sclabel, "scl");
        gtk_fixed_put(GTK_FIXED(bgfixed), sclabel, curx, cury+SHORTCUT_LABEL_TOPPENDING);
        
        // save widgets
        std::array<GtkWidget*, 2> sc;
        sc[0] = scbutton;
        sc[1] = sclabel;
        shortcuts.push_back(sc);            
        
        // caculate position of next app
        if(++xpos < xcount){
            curx += xstep;
        }
        else if(++ypos < ycount){
            xpos = 0;
            curx = xpadding+WINDOW_PADDING;
            cury += ystep;
        }
        else{
            break;
        }
    }
}

void refillShortCuts(const GdkRectangle &rect){
    // clean shortcuts
    for(size_t i=0; i<shortcuts.size();i++){
        gtk_container_remove(GTK_CONTAINER(bgfixed), shortcuts[i][0]);
        gtk_container_remove(GTK_CONTAINER(bgfixed), shortcuts[i][1]);
    }
    shortcuts.clear();
    
    // fill shortcuts
    fillShortCuts(rect);
}

GdkRectangle fitDeskRect(){
    // get monitor rectangle
    GdkRectangle rect;
    GdkDisplay *display = gdk_display_get_default();
    GdkWindow *root_window = gdk_get_default_root_window();
    GdkMonitor *monitor=gdk_display_get_monitor_at_window(display, root_window);
    gdk_monitor_get_workarea(monitor, &rect);
    
    // set desktop size and background size and also shortcuts
    if(rect.x>0 || rect.y>0){       // if screen moved by toolbar, move window to (0,0)
        rect.width += rect.x;
        rect.height += rect.y;
        rect.x = 0;
        rect.y = 0;
        gtk_window_move(GTK_WINDOW(window), 0, 0);
    }
    gtk_window_set_default_size(GTK_WINDOW(window), rect.width, rect.height);
    
    return rect;
}

void redrawBGImage(const GdkRectangle &rect){
    // build bgimage(by bgpixbuf)
    GError *err=NULL;
    std::string bgi = zddesktop.GetBackgroundImage();
    GdkPixbuf *bgpixbuf;
    if(bgi.empty()){
        bgpixbuf = gdk_pixbuf_new_from_file_at_scale(zddesktop.DefaultBGIDir.c_str(), rect.width, rect.height, false, &err);
        if(err!=NULL) return;
    }
    else {
        bgpixbuf = gdk_pixbuf_new_from_file_at_scale((zddesktop.BgiPath + bgi).c_str(), rect.width, rect.height, false, &err);
        if(err!=NULL){
            err = NULL;
            bgpixbuf = gdk_pixbuf_new_from_file_at_scale(zddesktop.DefaultBGIDir.c_str(), rect.width, rect.height, false, &err);
            if(err!=NULL) return;
        }
    }
    gtk_image_set_from_pixbuf(GTK_IMAGE(bgimage), bgpixbuf);
}

void screensize_Changed(GdkScreen *screen, gpointer data){
    GdkRectangle rect = fitDeskRect();
    redrawBGImage(rect);
    refillShortCuts(rect);
    
    gtk_widget_show_all(window);
    g_timeout_add(500, bypassInput, NULL);
}

void buildWindow(){
    // create window
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_type_hint(GTK_WINDOW(window), GDK_WINDOW_TYPE_HINT_DESKTOP);
    g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
      
    // set window default size by monitor size
    GdkRectangle rect = fitDeskRect();
        
    // scatch size change event of screen
    GdkScreen *screen = gdk_screen_get_default();
    g_signal_connect(screen, "size_changed", G_CALLBACK(screensize_Changed), NULL);
    
    // build css provider
    provider = gtk_css_provider_new();
    gtk_style_context_add_provider_for_screen(gtk_window_get_screen(GTK_WINDOW(window)), (GtkStyleProvider *)provider, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_css_provider_load_from_data(provider, CSS_DATA, -1, NULL);
    
    // set default background color of desktop
    addClassStyle(window, "bg");
    
    // build bgfixed, add bgimage and robotimage
    bgfixed = gtk_fixed_new();
    gtk_container_add(GTK_CONTAINER(window), bgfixed); 
    bgimage = gtk_image_new();
    gtk_fixed_put(GTK_FIXED(bgfixed), bgimage, 0, 0);
    robotimage = gtk_image_new();
    gtk_fixed_put(GTK_FIXED(bgfixed), robotimage, rect.width - ROBOT_DISPLAY_WIDTH, rect.height - ROBOT_DISPLAY_HEIGHT);
    gtk_widget_hide(robotimage);
    
    // set bgimage and fill shortcuts
    redrawBGImage(rect); 
    fillShortCuts(rect);
    
    // show window
    gtk_widget_show_all(window);
    
    // bypass input(like: mouse, key) event to root_window
    g_timeout_add(500, bypassInput, NULL);
}

bool refeshing = false;
void refreshBackground(int sig){
    if(!refeshing){
        refeshing = true;
        zddesktop.ReReadBgConf();
        GdkRectangle rect = fitDeskRect();
        redrawBGImage(rect);
        refeshing = false;
    }
}

gboolean bypassInputbyTag(gpointer user_data){
    bool hr = bypassInput(user_data);
    refeshing = false;
    return hr;
}
void refreshShortcut(int sig){
    if(!refeshing){
        refeshing = true;
        
        zddesktop.ReReadShortCuts();
        GdkRectangle rect = fitDeskRect();
        refillShortCuts(rect);
        
        gtk_widget_show_all(window);
        g_timeout_add(500, bypassInputbyTag, NULL);
    }
}

/*
bool writePid(){
    std::ofstream file(TEMP_PID_DIR);
    if(!file.is_open()) return false;
    
    int pid = getpid();
    file << pid;
    file.close();
    return true;
}
*/

int readPid(){
    std::vector<std::string> lines = ZDBase::ExecReturnLines("pgrep zddesk");
    int curpid = getpid();
    for(int i=0;i<lines.size();i++){
        int pid = std::atoi(lines[0].c_str());
        if(pid!=curpid)
            return pid;
    }
    return 0;
    
    /*
    // read pid from temp pid file
    std::ifstream pidfile(TEMP_PID_DIR);
    if(!pidfile.is_open()) return 0;        // do not have pidfile, return 0
    std::string pid;
    std::getline(pidfile, pid);
    if(pid.empty()) return 0;               // do not have pid in file, return 0
    pidfile.close();
    
    // check command of pid, if it is zddesk process
    std::ifstream commfile("/proc/"+pid+"/comm");
    if(!commfile.is_open()) return 0;      // the pid do not have a process, return 0
    std::string comm;
    std::getline(commfile, comm);
    if(comm!="zddesk") return 0; ;         // process is not zddesk process, return 0
    commfile.close();
    
    return std::atoi(pid.c_str());
    */
}


bool StartRobot(){
    GError* err = NULL;
    GdkPixbufAnimation *cominganim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotComingAnimDir.c_str(), &err);
    gtk_image_set_from_animation(GTK_IMAGE(robotimage), cominganim);
    gtk_widget_show(robotimage);
    usleep(500000);
    
    GdkPixbufAnimation *waitinganim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotWaitingAnimDir.c_str(), &err);
    gtk_image_set_from_animation(GTK_IMAGE(robotimage), waitinganim);
    gtk_widget_show(robotimage);
    usleep(500000);
    
    return true;
}

bool StopRobot(){
    GError* err = NULL;
    GdkPixbufAnimation *aianim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotLeavingAnimDir.c_str(), &err);
    gtk_image_set_from_animation (GTK_IMAGE(robotimage), aianim);
    gtk_widget_show(robotimage);
    usleep(500000);
    
    gtk_widget_hide(robotimage);
    
    return true;
}

bool RobotDo(struct lws *wsi, const std::string& action){
    std::string cmd = "";
    GError* err = NULL;
    if(ZDBase::StartWith(action, ROBOT_OPEN_APP_PREFIX)){
        std::string appname = action.substr(ROBOT_OPEN_APP_PREFIX.length());
        std::vector<ShortCut> scs = zddesktop.GetShortCuts();
        for(size_t i=0; i<scs.size();i++){
            if(appname == scs[i].Name){
                lws_write(wsi, (unsigned char *)ROBOT_OPEN_APP_SUCCESS.c_str(), ROBOT_OPEN_APP_SUCCESS.size(), LWS_WRITE_TEXT);
                
                GdkPixbufAnimation *yesanim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotYesAnimDir.c_str(), &err);
                gtk_image_set_from_animation(GTK_IMAGE(robotimage), yesanim);
                gtk_widget_show(robotimage);
                usleep(500000);
                
                GdkPixbufAnimation *waitinganim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotWaitingAnimDir.c_str(), &err);
                gtk_image_set_from_animation(GTK_IMAGE(robotimage), waitinganim);
                gtk_widget_show(robotimage);
                usleep(500000);
            
                if(scs[i].Type=="web"){
                    cmd = "firefox --new-tab '" + scs[i].Link + "' >/dev/null 2>&1 &";
                }
                else{
                    cmd = scs[i].Link + " >/dev/null 2>&1 &";
                }
                return ZDBase::Exec(cmd.c_str());
            }
        }
        
        lws_write(wsi, (unsigned char *)ROBOT_OPEN_APP_FAIL.c_str(), ROBOT_OPEN_APP_FAIL.size(), LWS_WRITE_TEXT);
        
        GdkPixbufAnimation *noanim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotNoAnimDir.c_str(), &err);
        gtk_image_set_from_animation(GTK_IMAGE(robotimage), noanim);
        gtk_widget_show(robotimage);
        usleep(500000);
        
        GdkPixbufAnimation *waitinganim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotWaitingAnimDir.c_str(), &err);
        gtk_image_set_from_animation(GTK_IMAGE(robotimage), waitinganim);
        gtk_widget_show(robotimage);
        usleep(500000);
        
        return false;
    }
    else{
        lws_write(wsi, (unsigned char *)ROBOT_WRITE_SUCCESS.c_str(), ROBOT_WRITE_SUCCESS.size(), LWS_WRITE_TEXT);
        
        GdkPixbufAnimation *yesanim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotYesAnimDir.c_str(), &err);
        gtk_image_set_from_animation(GTK_IMAGE(robotimage), yesanim);
        gtk_widget_show(robotimage);
        usleep(500000);
        
        GdkPixbufAnimation *waitinganim = gdk_pixbuf_animation_new_from_file(zddesktop.RobotWaitingAnimDir.c_str(), &err);
        gtk_image_set_from_animation(GTK_IMAGE(robotimage), waitinganim);
        gtk_widget_show(robotimage);
        usleep(500000);
        
        cmd = "xdotool type -delay 200 '%s' && xdotool key Return";
        std::string safeaction = ZDBase::ClearUnsafeChar(action);
        return ZDBase::Exec(cmd.c_str(), safeaction.c_str());
    }
}

static struct lws *authingWSI = NULL;
static struct lws *authedWSI = NULL;
static int callback_websocket(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len){
    switch(reason){
        case LWS_CALLBACK_ESTABLISHED:{
            if(authedWSI == NULL){
                if(!StartRobot()){
                    //printf("Start robot failed!\n");
                    return -1;
                }
            }
            authingWSI = wsi;
            //printf("Client connect!\n");
            break;
        }
        case LWS_CALLBACK_RECEIVE:{
            std::string instr = std::string((char *)in, len);
            if(wsi==authedWSI){
                if(!instr.empty())
                    RobotDo(wsi, instr);
            }
            else if(wsi==authingWSI){
                if(zserver.VerifyToken(instr)){
                    if(authedWSI!=NULL){
                        lws_callback_on_writable(authedWSI);
                    }
                    authedWSI = authingWSI;
                    authingWSI = NULL;
                }
                else {
                    authingWSI = NULL;
                    return -1;
                }
            }
            else {
                lws_callback_on_writable(wsi);
            }
            break;
        }
        case LWS_CALLBACK_CLOSED:{
            //if close by client normally, close display and set autherWSI to NULL. else if new authedWSI set by authingWSI, wsi is different from authedWSI, don't need to set NULL.
            if(wsi == authedWSI){
                StopRobot();
                authedWSI = NULL;
            }
            //printf("Client close!\n");
            break;
        }
        case LWS_CALLBACK_SERVER_WRITEABLE:{
            return -1;
        }
        default:
            break;
    }
    
    return 0;
}

struct lws_protocols protocols[] = {
    {"ws", callback_websocket, 0, 0},
    {NULL, NULL, 0, 0}
};

int exit_robot_ws = 1;
void runRobotWebSocket(){
    // run websocket service
    int port = zddesktop.GetRobotPort();
    struct lws_context_creation_info info;
    memset(&info, 0, sizeof(info));
    info.port = port;
    info.protocols = protocols;
    struct lws_context *context = lws_create_context(&info);
    if(!context){
        printf("Create websocket context error!\n");
        return;
    }
    printf("Websocket start on :%d!\n", port);
    while(exit_robot_ws){
        lws_service(context, 50);
    }
    lws_context_destroy(context);
    printf("Websocket stop!\n");
}

int runDeamon(){
    // check and write pid, to makesure only one deamon process
    if(readPid()>0){
        printf("Desk deamon is already exist\n");
        return 1;
    }
    /*
    if(!writePid()){
        printf("Save PID failed\n");
        return 1;
    }
    */
    
    // regist signals
    if(signal(SIGUSR1, refreshBackground)==SIG_ERR){
        printf("Signal regist failed\n");
        return 1;
    }
    if(signal(SIGUSR2, refreshShortcut)==SIG_ERR){
        printf("Signal regist failed\n");
        return 1;
    }
    
    // start WebSocket
    if(zserver.IsAvailable())
        g_thread_new("robot", (GThreadFunc)runRobotWebSocket, NULL); 
    
    // run window
    gtk_init(NULL, NULL);
    buildWindow();
    gtk_main();
    
    // stop WebSocket
    if(zserver.IsAvailable())
        exit_robot_ws = 0;
    
    return 0;
}

int main(int argc, char *argv[]) {    
    if(argc <= 1){
        printf("Add arguments as follow:\n-D: run Desk deamon\n-RBG: refesh background\n-RSC: refesh shortcuts\n");
    }
    else {
        bool rbgnotdone = true, rscnotdone = true;
        for(int i=1; i<argc; i++){
            std::string arg = argv[i];
            if(arg=="-D"){
                return runDeamon();
            }
            else if((arg=="-RBG") && rbgnotdone){
                int pid = readPid();
                if(pid==0){
                    printf("read PID failed\n");
                    return 1;
                }
                if(kill(pid, SIGUSR1)!=0){
                    printf("send signal failed\n");
                    return 1;
                }
                rbgnotdone = false;
            }
            else if((arg=="-RSC") && rscnotdone){
                int pid = readPid();
                if(pid==0){
                    printf("read PID failed\n");
                    return 1;
                }
                if(kill(pid, SIGUSR2)!=0){
                    printf("send signal failed\n");
                    return 1;
                }
                rscnotdone = false;
            }
        }
    }
    
    return 0;
}


