// Remember.cc for Fluxbox Window Manager
// Copyright (c) 2002 Xavier Brouckaert
// Copyright (c) 2003 Henrik Kinnunen (fluxgen at users.sourceforge.net)
//                and Simon Bowden    (rathnor at users.sourceforge.net)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

// $Id: Remember.cc,v 1.24 2003/06/12 15:12:19 rathnor Exp $

#include "Remember.hh"
#include "ClientPattern.hh"
#include "StringUtil.hh"
#include "Screen.hh"
#include "Window.hh"
#include "WinClient.hh"
#include "FbMenu.hh"
#include "MenuItem.hh"
#include "App.hh"

#include <X11/Xlib.h>

//use GNU extensions
#ifndef	 _GNU_SOURCE
#define	 _GNU_SOURCE
#endif // _GNU_SOURCE


#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
#include <memory>

using namespace std;

#ifndef    MAXPATHLEN
#define    MAXPATHLEN 255
#endif // MAXPATHLEN

namespace {

class RememberMenuItem : public FbTk::MenuItem {
public:
    RememberMenuItem(const char *label, Remember &remember,
                     FluxboxWindow &fbwin,
                     Remember::Attribute attrib) :
        FbTk::MenuItem(label), m_remember(remember), 
        m_win(fbwin), m_attrib(attrib) {}

    bool isSelected() const {
        return m_remember.isRemembered(m_win.winClient(), m_attrib);
    }

    bool isEnabled() const {
        if (m_attrib != Remember::REM_JUMPWORKSPACE) 
            return true;
        else 
            return (m_remember.isRemembered(m_win.winClient(), Remember::REM_WORKSPACE));
    }

    void click(int button, int time) {
        if (isSelected()) {
            m_remember.forgetAttrib(m_win.winClient(), m_attrib);
        } else {
            m_remember.rememberAttrib(m_win.winClient(), m_attrib);
        }
        m_remember.save();
        FbTk::MenuItem::click(button, time);
    }

private:
    // my remember manager
    Remember &m_remember;
    FluxboxWindow &m_win;
    Remember::Attribute m_attrib;
};

FbTk::Menu *createRememberMenu(Remember &remember, FluxboxWindow &win) {
    // each fluxboxwindow has its own windowmenu
    // so we also create a remember menu just for it...
    BScreen &screen = win.screen();
    FbTk::Menu *menu = new FbMenu(*screen.menuTheme(), 
                                  screen.screenNumber(), 
                                  screen.imageControl(), 
                                  *screen.layerManager().getLayer(Fluxbox::instance()->getMenuLayer()));
    menu->disableTitle();
    // TODO: nls
    menu->insert(new RememberMenuItem("Workspace", remember, win,
                                      Remember::REM_WORKSPACE));
    menu->insert(new RememberMenuItem("Jump to workspace", remember, win,
                                      Remember::REM_JUMPWORKSPACE));
    menu->insert(new RememberMenuItem("Dimensions", remember, win,
                                      Remember::REM_DIMENSIONS));
    menu->insert(new RememberMenuItem("Position", remember, win,
                                      Remember::REM_POSITION));
    menu->insert(new RememberMenuItem("Sticky", remember, win,
                                      Remember::REM_STUCKSTATE));
    menu->insert(new RememberMenuItem("Decorations", remember, win,
                                      Remember::REM_DECOSTATE));
    menu->insert(new RememberMenuItem("Shaded", remember, win,
                                      Remember::REM_SHADEDSTATE));
    menu->insert(new RememberMenuItem("Layer", remember, win,
                                      Remember::REM_LAYER));
    //    menu->insert(new RememberMenuItem("Tab", remember, win,
    //                                     Remember::REM_TABSTATE));
    menu->insert(new RememberMenuItem("Save on close", remember, win,
                                      Remember::REM_SAVEONCLOSE));

    menu->update();
    return menu;
};

}; // end anonymous namespace

Application::Application() {
    workspace_remember =
	dimensions_remember =
	position_remember =
	stuckstate_remember =
	decostate_remember =
	shadedstate_remember =
	tabstate_remember =
	jumpworkspace_remember =
        layer_remember =
	save_on_close_remember = false;
}

/********************************************************
 * Remember *
 ************/

Remember::Remember() {
    load();
}

Remember::~Remember() {
    // free our resources

    // the patterns free the "Application"s
    // the client mapping shouldn't need cleaning
    Patterns::iterator it;
    while (!m_pats.empty()) {
        it = m_pats.begin();
        delete it->first; // ClientPattern
        delete it->second; // Application
        m_pats.erase(it);
    }
}

Application* Remember::find(WinClient &winclient) {
    // if it is already associated with a application, return that one
    // otherwise, check it against every pattern that we've got
    Clients::iterator wc_it = m_clients.find(&winclient);
    if (wc_it != m_clients.end())
        return wc_it->second;
    else {
        Patterns::iterator it = m_pats.begin();
        for (; it != m_pats.end(); it++) 
            if (it->first->match(winclient)) {
                it->first->addMatch();
                m_clients[&winclient] = it->second;
                return it->second;
            }
    }
    // oh well, no matches
    return 0;
}

Application * Remember::add(WinClient &winclient) {
    ClientPattern *p = new ClientPattern();
    Application *app = new Application();
    // by default, we match against the WMClass of a window.
    p->addTerm(p->getProperty(ClientPattern::NAME, winclient), ClientPattern::NAME);
    m_clients[&winclient] = app;
    p->addMatch();
    m_pats.push_back(make_pair(p, app));
    return app;
}

int Remember::parseApp(ifstream &file, Application &app) {
    string line;
    int row = 0;
    while (! file.eof()) {
        if (getline(file, line)) {
            row++;
            if (line[0] != '#') { //the line is commented
                int parse_pos = 0, err = 0;
                string str_key, str_label;
                err = FbTk::StringUtil::getStringBetween(str_key, 
                                                         line.c_str(), 
                                                         '[', ']');
                if (err > 0 ) {
                    parse_pos += err;
                    err = FbTk::StringUtil::getStringBetween(str_label, 
                                                       line.c_str() + parse_pos, 
                                                       '{', '}');
                    if (err>0) {
                        parse_pos += err;
                    }
                } else
                    continue; //read next line

                if (!str_key.size())
                    continue; //read next line
                if (str_key == "Workspace") {
                    unsigned int w;
                    istringstream iss(str_label.c_str());
                    iss >> w;
                    app.rememberWorkspace(w);
                } else if (str_key == "Layer") {
                    unsigned int l;
                    istringstream iss(str_label.c_str());
                    iss >> l;
                    app.rememberLayer(l);
                } else if (str_key == "Dimensions") {
                    unsigned int h,w;
                    istringstream iss(str_label.c_str());
                    iss >> w >> h;
                    app.rememberDimensions(w,h);
                } else if (str_key == "Position") {
                    unsigned int x,y;
                    istringstream iss(str_label);
                    iss >> x >> y;
                    app.rememberPosition(x,y);
                } else if (str_key == "Shaded") {
                    app.rememberShadedstate((str_label=="yes"));
                } else if (str_key == "Tab") {
                    app.rememberTabstate((str_label=="yes"));
                } else if (str_key == "Deco") {
                    if (str_label == "NONE") {
                        app.rememberDecostate((unsigned int) 0);
                    } else if (str_label == "NORMAL") {
                        app.rememberDecostate((unsigned int) 0xfffffff);
                    } else if (str_label == "TINY") {
                        app.rememberDecostate((unsigned int)
                                             FluxboxWindow::DECORM_TITLEBAR 
                                             | FluxboxWindow::DECORM_ICONIFY
                                             | FluxboxWindow::DECORM_MENU
                                             );
                    } else if (str_label == "TOOL") {
                        app.rememberDecostate((unsigned int)
                                             FluxboxWindow::DECORM_TITLEBAR
                                             | FluxboxWindow::DECORM_MENU
                                             );
                    } else if (str_label == "BORDER") {
                        app.rememberDecostate((unsigned int)
                                             FluxboxWindow::DECORM_BORDER
                                             | FluxboxWindow::DECORM_MENU
                                             );
                    } else {
                        unsigned int mask;
                        const char * str = str_label.c_str();
                        // it'll have at least one char and \0, so this is safe
                        istringstream iss(str);
                        // check for hex
                        if (str[0] == '0' && str[1] == 'x') {
                            iss.seekg(2);
                            iss >> hex;
                        }
                        iss >> mask ;
                        app.rememberDecostate(mask);
                    }
                } else if (str_key == "Sticky") {
                    app.rememberStuckstate((str_label=="yes"));
                } else if (str_key == "Jump") {
                    app.rememberJumpworkspace((str_label=="yes"));
                } else if (str_key == "Close") {
                    app.rememberSaveOnClose((str_label=="yes"));
                } else if (str_key == "end") {
                    return row;
                } else {
                    cerr << "Unsupported apps key = " << str_key << endl;
                }
            }
        }
    }
    return row;
}

void Remember::load() {

    string apps_string;
    Fluxbox::instance()->getDefaultDataFilename("apps", apps_string);

#ifdef DEBUG
    cerr<<__FILE__<<"("<<__FUNCTION__<<"): Loading apps file ["<<apps_string<<"]"<<endl;
#endif // DEBUG
    ifstream apps_file(apps_string.c_str());

    if (!apps_file.fail()) {
        if (!apps_file.eof()) {
            string line;
            int row = 0;
            while (getline(apps_file, line) && ! apps_file.eof()) {
                row++;
                if (line[0] == '#')
                    continue;
                string key;
                int err=0;
                int pos = FbTk::StringUtil::getStringBetween(key, 
                                                             line.c_str(), 
                                                             '[', ']');

                if (pos > 0 && key == "app") {
                    ClientPattern *pat = new ClientPattern(line.c_str() + pos);
                    if ((err = pat->error()) == 0) {
                        Application *app = new Application();
                        m_pats.push_back(make_pair(pat, app));
                        row += parseApp(apps_file, *app);
                    } else {
                        cerr<<"Error reading apps file at line "<<row<<", column "<<(err+pos)<<"."<<endl;
                        delete pat; // since it didn't work
                    }
                } else
                    cerr<<"Error in apps file on line "<<row<<"."<<endl;
                
            }
        } else {
#ifdef DEBUG
            cerr<<__FILE__<<"("<<__FUNCTION__<< ") Empty apps file" << endl;
#endif
        }
    } else {
        cerr << "apps file failure" << endl;
    }
}

void Remember::save() {
#ifdef DEBUG
    cerr<<__FILE__<<"("<<__FUNCTION__<<"): Saving apps file..."<<endl;
#endif // DEBUG
    string apps_string;
    Fluxbox::instance()->getDefaultDataFilename("apps", apps_string);
    ofstream apps_file(apps_string.c_str());
    Patterns::iterator it = m_pats.begin();
    Patterns::iterator it_end = m_pats.end();
    for (; it != it_end; ++it) {
        apps_file << "[app]"<<it->first->toString()<<endl;
        Application &a = *it->second;
        if (a.workspace_remember) {
            apps_file << "  [Workspace]\t{" << a.workspace << "}" << endl;
        }
        if (a.dimensions_remember) {
            apps_file << "  [Dimensions]\t{" << a.w << " " << a.h << "}" << endl;
        }
        if (a.position_remember) {
            apps_file << "  [Position]\t{" << a.x << " " << a.y << "}" << endl;
        }
        if (a.shadedstate_remember) {
            apps_file << "  [Shaded]\t{" << ((a.shadedstate)?"yes":"no") << "}" << endl;
        }
        if (a.tabstate_remember) {
            apps_file << "  [Tab]\t\t{" << ((a.tabstate)?"yes":"no") << "}" << endl;
        }
        if (a.decostate_remember) {
            switch (a.decostate) {
            case (0) :
                apps_file << "  [Deco]\t{NONE}" << endl; 
                break;
            case (0xffffffff):
            case (FluxboxWindow::DECORM_LAST - 1):
                apps_file << "  [Deco]\t{NORMAL}" << endl;
                break;
            case (FluxboxWindow::DECORM_TITLEBAR 
                  | FluxboxWindow::DECORM_ICONIFY
                  | FluxboxWindow::DECORM_MENU):
                apps_file << "  [Deco]\t{TOOL}" << endl;
                break;
            case (FluxboxWindow::DECORM_TITLEBAR 
                  | FluxboxWindow::DECORM_MENU):
                apps_file << "  [Deco]\t{TINY}" << endl;
                break;
            case (FluxboxWindow::DECORM_BORDER 
                  | FluxboxWindow::DECORM_MENU):
                apps_file << "  [Deco]\t{BORDER}" << endl;
                break;
            default:
                apps_file << "  [Deco]\t{0x"<<hex<<a.decostate<<dec<<"}"<<endl;
                break;
            }
        }
        if (a.stuckstate_remember) {
            apps_file << "  [Sticky]\t{" << ((a.stuckstate)?"yes":"no") << "}" << endl;
        }
        if (a.jumpworkspace_remember) {
            apps_file << "  [Jump]\t{" << ((a.jumpworkspace)?"yes":"no") << "}" << endl;
        }
        if (a.layer_remember) {
            apps_file << "  [Layer]\t{" << a.layer << "}" << endl;
        }
        if (a.save_on_close_remember) {
            apps_file << "  [Close]\t{" << ((a.save_on_close)?"yes":"no") << "}" << endl;
        }
        apps_file << "[end]" << endl;
    }
}

bool Remember::isRemembered(WinClient &winclient, Attribute attrib) {
    Application *app = find(winclient);
    if (!app) return false;
    switch (attrib) {
    case REM_WORKSPACE:
        return app->workspace_remember;
        break;
    case REM_DIMENSIONS:
        return app->dimensions_remember;
        break;
    case REM_POSITION:
        return app->position_remember;
        break;
    case REM_STUCKSTATE:
        return app->stuckstate_remember;
        break;
    case REM_DECOSTATE:
        return app->decostate_remember;
        break;
    case REM_SHADEDSTATE:
        return app->shadedstate_remember;
        break;
        //    case REM_TABSTATE:
        //        return app->tabstate_remember;
        //        break;
    case REM_JUMPWORKSPACE:
        return app->jumpworkspace_remember;
        break;
    case REM_LAYER:
        return app->layer_remember;
        break;
    case REM_SAVEONCLOSE:
        return app->save_on_close_remember;
        break;
    case REM_LASTATTRIB:
    default:
        return false; // should never get here
    }
}

void Remember::rememberAttrib(WinClient &winclient, Attribute attrib) {
    FluxboxWindow *win = winclient.fbwindow();
    if (!win) return;
    Application *app = find(winclient);
    if (!app) {
        app = add(winclient);
        if (!app) return;
    }
    switch (attrib) {
    case REM_WORKSPACE:
        app->rememberWorkspace(win->workspaceNumber());
        break;
    case REM_DIMENSIONS:
        app->rememberDimensions(win->width(), win->height());
        break;
    case REM_POSITION:
        app->rememberPosition(win->x(), win->y());
        break;
    case REM_SHADEDSTATE:
        app->rememberShadedstate(win->isShaded());
        break;
    case REM_DECOSTATE:
        app->rememberDecostate(win->decorationMask());
        break;
    case REM_STUCKSTATE:
        app->rememberStuckstate(win->isStuck());
        break;
        //    case REM_TABSTATE:
        //        break;
    case REM_JUMPWORKSPACE:
        app->rememberJumpworkspace(true);
        break;
    case REM_LAYER:
        app->rememberLayer(win->layerNum());
        break;
    case REM_SAVEONCLOSE:
        app->rememberSaveOnClose(true);
        break;
    case REM_LASTATTRIB:
    default:
        // nothing
        break;
    }
}

void Remember::forgetAttrib(WinClient &winclient, Attribute attrib) {
    FluxboxWindow *win = winclient.fbwindow();
    if (!win) return;
    Application *app = find(winclient);
    if (!app) {
        app = add(winclient);
        if (!app) return;
    }
    switch (attrib) {
    case REM_WORKSPACE:
        app->forgetWorkspace();
        break;
    case REM_DIMENSIONS:
        app->forgetDimensions();
        break;
    case REM_POSITION:
        app->forgetPosition();
        break;
    case REM_STUCKSTATE:
        app->forgetStuckstate();
        break;
    case REM_DECOSTATE:
        app->forgetDecostate();
        break;
    case REM_SHADEDSTATE:
        app->forgetShadedstate();
        break;
//    case REM_TABSTATE:
//        break;
    case REM_JUMPWORKSPACE:
        app->forgetJumpworkspace();
        break;
    case REM_LAYER:
        app->forgetLayer();
        break;
    case REM_SAVEONCLOSE:
        app->forgetSaveOnClose();
        break;
    case REM_LASTATTRIB:
    default:
        // nothing
        break;
    }
}

void Remember::setupWindow(FluxboxWindow &win) {
    WinClient &winclient = win.winClient();

    // we don't touch the window if it is a transient
    // of something else
    int menupos = win.menu().numberOfItems()-2;
    if (menupos < -1)
        menupos = -1;

    if (winclient.transientFor()) {
        // still put something in the menu so people don't get confused
        // so, we add a disabled item...
        // TODO: nls
        FbTk::MenuItem *item = new FbTk::MenuItem("Remember...");
        item->setEnabled(false);
        win.menu().insert(item, menupos);
        win.menu().update();
        return;
    }

    // add the menu, this -2 is somewhat dodgy... :-/
    // All windows get the remember menu.
    // TODO: nls
    win.menu().insert("Remember...", 
                               createRememberMenu(*this, win), 
                               menupos);
    win.menu().reconfigure();

    Application *app = find(winclient);
    if (app == 0) 
        return; // nothing to do

    BScreen &screen = win.screen();

    if (app->workspace_remember) {
        // TODO: fix placement to initialise properly
        screen.reassociateWindow(&win, app->workspace, true); 
        if (app->jumpworkspace_remember)
            screen.changeWorkspaceID(app->workspace);
    }

    if (app->dimensions_remember)
        win.resize(app->w, app->h);
    
    if (app->position_remember)
        win.move(app->x, app->y);

    if (app->shadedstate_remember)
        // if inconsistent...
        if (win.isShaded() && !app->shadedstate ||
            !win.isShaded() && app->shadedstate)
            win.shade(); // toggles

    // external tabs aren't available atm...
    //if (app->tabstate_remember) ...

    if (app->decostate_remember)
        win.setDecorationMask(app->decostate);

    if (app->stuckstate_remember)
        // if inconsistent...
        if (win.isStuck() && !app->stuckstate ||
            !win.isStuck() && app->stuckstate)
            win.stick(); // toggles

    if (app->layer_remember)
        win.moveToLayer(app->layer);

}

void Remember::updateWindowClose(FluxboxWindow &win) {
    // This doesn't work at present since fluxbox.cc is missing the windowclose stuff.
    // I don't trust it (particularly winClient()) while this is the case

    return;

    WinClient &winclient = win.winClient();
    Application *app = find(winclient);
    Clients::iterator wc_it = m_clients.find(&win.winClient());

    if (wc_it != m_clients.end())
        m_clients.erase(wc_it);

    if (!app || !(app->save_on_close_remember && app->save_on_close))
        return;

    for (int attrib = 0; attrib <= REM_LASTATTRIB; attrib++) {
        if (isRemembered(winclient, (Attribute) attrib)) {
            rememberAttrib(winclient, (Attribute) attrib);
        }
    }

    save();
}