/////////////////////////////////////////////////////////////////////////////
// Name:        samples/fswatcher/fswatcher.cpp
// Purpose:     wxFileSystemWatcher sample
// Author:      Bartosz Bekier
// Created:     2009-06-27
// Copyright:   (c) Bartosz Bekier
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

#include "wx/wxprec.h"

#ifndef WX_PRECOMP
    #include "wx/wx.h"
#endif

#ifndef wxHAS_IMAGES_IN_RESOURCES
    #include "../sample.xpm"
#endif

#include "wx/fswatcher.h"
#include "wx/listctrl.h"
#include "wx/cmdline.h"

// Define a new frame type: this is going to be our main frame
class MyFrame : public wxFrame
{
public:
    MyFrame(const wxString& title);
    virtual ~MyFrame();

    // Add an entry of the specified type asking the user for the filename if
    // the one passed to this function is empty.
    void AddEntry(wxFSWPathType type, wxString filename = wxString());

    bool CreateWatcherIfNecessary();

private:
    // file system watcher creation
    void CreateWatcher();

    // event handlers
    void OnClear(wxCommandEvent& WXUNUSED(event)) { m_evtConsole->Clear(); }
    void OnQuit(wxCommandEvent& WXUNUSED(event)) { Close(true); }
    void OnWatch(wxCommandEvent& event);
    void OnFollowLinks(wxCommandEvent& event);
    void OnAbout(wxCommandEvent& event);

    void OnAdd(wxCommandEvent& event);
    void OnAddTree(wxCommandEvent& event);
    void OnRemove(wxCommandEvent& event);
    void OnRemoveAll(wxCommandEvent& WXUNUSED(event));
    void OnRemoveUpdateUI(wxUpdateUIEvent& event);
    void OnRemoveAllUpdateUI(wxUpdateUIEvent& event);

    void OnFileSystemEvent(wxFileSystemWatcherEvent& event);
    void LogEvent(const wxFileSystemWatcherEvent& event);

    wxTextCtrl *m_evtConsole;         // events console
    wxListView *m_filesList;          // list of watched paths
    wxFileSystemWatcher* m_watcher;   // file system watcher
    bool m_followLinks;               // should symlinks be dereferenced

    const static wxString LOG_FORMAT; // how to format events
};

const wxString MyFrame::LOG_FORMAT = " %-12s %-36s    %-36s";

// Define a new application type, each program should derive a class from wxApp
class MyApp : public wxApp
{
public:
    // 'Main program' equivalent: the program execution "starts" here
    virtual bool OnInit() override
    {
        if ( !wxApp::OnInit() )
            return false;

        wxLog::AddTraceMask("EventSource");
        wxLog::AddTraceMask(wxTRACE_FSWATCHER);

        // create the main application window
        m_frame = new MyFrame("File System Watcher wxWidgets App");

        // If we returned false here, the application would exit immediately.
        return true;
    }

    // create the file system watcher here, because it needs an active loop
    virtual void OnEventLoopEnter(wxEventLoopBase* WXUNUSED(loop)) override
    {
        if ( m_frame->CreateWatcherIfNecessary() )
        {
            if ( !m_dirToWatch.empty() )
                m_frame->AddEntry(wxFSWPath_Dir, m_dirToWatch);
        }
    }

    virtual void OnInitCmdLine(wxCmdLineParser& parser) override
    {
        wxApp::OnInitCmdLine(parser);
        parser.AddParam("directory to watch",
                        wxCMD_LINE_VAL_STRING,
                        wxCMD_LINE_PARAM_OPTIONAL);
    }

    virtual bool OnCmdLineParsed(wxCmdLineParser& parser) override
    {
        if ( !wxApp::OnCmdLineParsed(parser) )
            return false;

        if ( parser.GetParamCount() )
            m_dirToWatch = parser.GetParam();

        return true;
    }

private:
    MyFrame *m_frame;

    // The directory to watch if specified on the command line.
    wxString m_dirToWatch;
};

// Create a new application object: this macro will allow wxWidgets to create
// the application object during program execution (it's better than using a
// static object for many reasons) and also declares the accessor function
// wxGetApp() which will return the reference of the right type (i.e. MyApp and
// not wxApp)
wxIMPLEMENT_APP(MyApp);


// ============================================================================
// implementation
// ============================================================================

// frame constructor
MyFrame::MyFrame(const wxString& title)
    : wxFrame(nullptr, wxID_ANY, title),
      m_watcher(nullptr), m_followLinks(false)
{
    SetIcon(wxICON(sample));

    // IDs for menu and buttons
    enum
    {
        MENU_ID_QUIT = wxID_EXIT,
        MENU_ID_CLEAR = wxID_CLEAR,
        MENU_ID_WATCH = 101,
        MENU_ID_DEREFERENCE,

        BTN_ID_ADD = 200,
        BTN_ID_ADD_TREE,
        BTN_ID_REMOVE,
        BTN_ID_REMOVE_ALL
    };

    // ================================================================
    // menu

    // create a menu bar
    wxMenu *menuFile = new wxMenu;
    menuFile->Append(MENU_ID_CLEAR, "&Clear log\tCtrl-L");
    menuFile->AppendSeparator();
    menuFile->Append(MENU_ID_QUIT, "E&xit\tAlt-X", "Quit this program");

    // "Watch" menu
    wxMenu *menuMon = new wxMenu;
    wxMenuItem* it = menuMon->AppendCheckItem(MENU_ID_WATCH, "&Watch\tCtrl-W");
    // started by default, because file system watcher is started by default
    it->Check(true);

#if defined(__UNIX__)
    // Let the user decide whether to dereference symlinks. If he makes the
    // wrong choice, asserts will occur if the symlink target is also watched
    it = menuMon->AppendCheckItem(MENU_ID_DEREFERENCE,
                                  "&Follow symlinks\tCtrl-F",
                                  _("If checked, dereference symlinks")
                                 );
    it->Check(false);
    Bind(wxEVT_MENU, &MyFrame::OnFollowLinks, this, MENU_ID_DEREFERENCE);
#endif // __UNIX__

    // the "About" item should be in the help menu
    wxMenu *menuHelp = new wxMenu;
    menuHelp->Append(wxID_ABOUT, "&About\tF1", "Show about dialog");

    // now append the freshly created menu to the menu bar...
    wxMenuBar *menuBar = new wxMenuBar();
    menuBar->Append(menuFile, "&File");
    menuBar->Append(menuMon, "&Watch");
    menuBar->Append(menuHelp, "&Help");

    // ... and attach this menu bar to the frame
    SetMenuBar(menuBar);

    // ================================================================
    // upper panel

    // panel
    wxPanel *panel = new wxPanel(this);
    wxSizer *panelSizer = new wxGridSizer(2);
    wxBoxSizer *leftSizer = new wxBoxSizer(wxVERTICAL);

    // label
    wxStaticText* label = new wxStaticText(panel, wxID_ANY, "Watched paths");
    leftSizer->Add(label, wxSizerFlags().Center().Border(wxALL));

    // list of files
    m_filesList = new wxListView(panel, wxID_ANY, wxPoint(-1,-1),
                                 wxSize(300,200), wxLC_LIST | wxLC_SINGLE_SEL);
    leftSizer->Add(m_filesList, wxSizerFlags(1).Expand());

    // buttons
    wxButton* buttonAdd = new wxButton(panel, BTN_ID_ADD, "&Add");
    wxButton* buttonAddTree = new wxButton(panel, BTN_ID_ADD_TREE, "Add &tree");
    wxButton* buttonRemove = new wxButton(panel, BTN_ID_REMOVE, "&Remove");
    wxButton* buttonRemoveAll = new wxButton(panel, BTN_ID_REMOVE_ALL, "Remove a&ll");
    wxSizer *btnSizer = new wxGridSizer(2);
    btnSizer->Add(buttonAdd, wxSizerFlags().Center().Border(wxALL));
    btnSizer->Add(buttonAddTree, wxSizerFlags().Center().Border(wxALL));
    btnSizer->Add(buttonRemove, wxSizerFlags().Center().Border(wxALL));
    btnSizer->Add(buttonRemoveAll, wxSizerFlags().Center().Border(wxALL));

    // and put it all together
    leftSizer->Add(btnSizer, wxSizerFlags(0).Expand());
    panelSizer->Add(leftSizer, wxSizerFlags(1).Expand());
    panel->SetSizerAndFit(panelSizer);

    // ================================================================
    // lower panel

    wxTextCtrl *headerText = new wxTextCtrl(this, wxID_ANY, "",
                                            wxDefaultPosition, wxDefaultSize,
                                            wxTE_READONLY);
    wxString h = wxString::Format(LOG_FORMAT, "event", "path", "new path");
    headerText->SetValue(h);

    // event console
    m_evtConsole = new wxTextCtrl(this, wxID_ANY, "",
                               wxDefaultPosition, wxSize(200,200),
                               wxTE_MULTILINE|wxTE_READONLY|wxHSCROLL);

    // set monospace font to have output in nice columns
    wxFont font(wxFontInfo(9).Family(wxFONTFAMILY_TELETYPE));
    headerText->SetFont(font);
    m_evtConsole->SetFont(font);

    // ================================================================
    // laying out whole frame

    wxBoxSizer *sizer = new wxBoxSizer(wxVERTICAL);
    sizer->Add(panel, wxSizerFlags(1).Expand());
    sizer->Add(headerText, wxSizerFlags().Expand());
    sizer->Add(m_evtConsole, wxSizerFlags(1).Expand());
    SetSizerAndFit(sizer);

    // set size and position on screen
    SetSize(800, 600);
    CentreOnScreen();

    // ================================================================
    // event handlers & show

    // menu
    Bind(wxEVT_MENU, &MyFrame::OnClear, this, MENU_ID_CLEAR);
    Bind(wxEVT_MENU, &MyFrame::OnQuit, this, MENU_ID_QUIT);
    Bind(wxEVT_MENU, &MyFrame::OnWatch, this, MENU_ID_WATCH);
    Bind(wxEVT_MENU, &MyFrame::OnAbout, this, wxID_ABOUT);

    // buttons
    Bind(wxEVT_BUTTON, &MyFrame::OnAdd, this, BTN_ID_ADD);
    Bind(wxEVT_BUTTON, &MyFrame::OnAddTree, this, BTN_ID_ADD_TREE);
    Bind(wxEVT_BUTTON, &MyFrame::OnRemove, this, BTN_ID_REMOVE);
    Bind(wxEVT_UPDATE_UI, &MyFrame::OnRemoveUpdateUI, this, BTN_ID_REMOVE);
    Bind(wxEVT_BUTTON, &MyFrame::OnRemoveAll, this, BTN_ID_REMOVE_ALL);
    Bind(wxEVT_UPDATE_UI, &MyFrame::OnRemoveAllUpdateUI, this, BTN_ID_REMOVE_ALL);

    // and show itself (the frames, unlike simple controls, are not shown when
    // created initially)
    Show(true);
}

MyFrame::~MyFrame()
{
    delete m_watcher;
}

bool MyFrame::CreateWatcherIfNecessary()
{
    if (m_watcher)
        return false;

    CreateWatcher();
    Bind(wxEVT_FSWATCHER, &MyFrame::OnFileSystemEvent, this);

    return true;
}

void MyFrame::CreateWatcher()
{
    wxCHECK_RET(!m_watcher, "Watcher already initialized");
    m_watcher = new wxFileSystemWatcher();
    m_watcher->SetOwner(this);
}

// ============================================================================
// event handlers
// ============================================================================

void MyFrame::OnAbout(wxCommandEvent& WXUNUSED(event))
{
    wxMessageBox("Demonstrates the usage of file system watcher, "
                 "the wxWidgets monitoring system notifying you of "
                 "changes done to your files.\n"
                 "(c) 2009 Bartosz Bekier\n",
                 "About wxWidgets File System Watcher Sample",
                 wxOK | wxICON_INFORMATION, this);
}

void MyFrame::OnWatch(wxCommandEvent& event)
{
    wxLogDebug("%s start=%d", __func__, event.IsChecked());

    if (event.IsChecked())
    {
        wxCHECK_RET(!m_watcher, "Watcher already initialized");
        CreateWatcher();
    }
    else
    {
        wxCHECK_RET(m_watcher, "Watcher not initialized");
        m_filesList->DeleteAllItems();
        wxDELETE(m_watcher);
    }
}

void MyFrame::OnFollowLinks(wxCommandEvent& event)
{
    m_followLinks = event.IsChecked();
}

void MyFrame::OnAdd(wxCommandEvent& WXUNUSED(event))
{
    AddEntry(wxFSWPath_Dir);
}

void MyFrame::OnAddTree(wxCommandEvent& WXUNUSED(event))
{
    AddEntry(wxFSWPath_Tree);
}

void MyFrame::AddEntry(wxFSWPathType type, wxString filename)
{
    if ( filename.empty() )
    {
        // TODO account for adding the files as well
        filename = wxDirSelector("Choose a folder to watch", "",
                                 wxDD_DEFAULT_STYLE | wxDD_DIR_MUST_EXIST);
        if ( filename.empty() )
            return;
    }

    wxCHECK_RET(m_watcher, "Watcher not initialized");

    wxLogDebug("Adding %s: '%s'",
               filename,
               type == wxFSWPath_Dir ? "directory" : "directory tree");

    wxString prefix;
    bool ok = false;

    // This will tell wxFileSystemWatcher whether to dereference symlinks
    wxFileName fn = wxFileName::DirName(filename);
    if (!m_followLinks)
    {
        fn.DontFollowLink();
    }

    switch ( type )
    {
        case wxFSWPath_Dir:
            ok = m_watcher->Add(fn);
            prefix = "Dir:  ";
            break;

        case wxFSWPath_Tree:
            ok = m_watcher->AddTree(fn);
            prefix = "Tree: ";
            break;

        case wxFSWPath_File:
        case wxFSWPath_None:
            wxFAIL_MSG( "Unexpected path type." );
    }

    if (!ok)
    {
        wxLogError("Error adding '%s' to watched paths", filename);
        return;
    }

    // Prepend 'prefix' to the filepath, partly for display
    // but mostly so that OnRemove() can work out the correct way to remove it
    m_filesList->InsertItem(m_filesList->GetItemCount(),
                            prefix + wxFileName::DirName(filename).GetFullPath());
}

void MyFrame::OnRemove(wxCommandEvent& WXUNUSED(event))
{
    wxCHECK_RET(m_watcher, "Watcher not initialized");
    long idx = m_filesList->GetFirstSelected();
    if (idx == -1)
        return;

    bool ret = false;
    wxString path = m_filesList->GetItemText(idx).Mid(6);

    // This will tell wxFileSystemWatcher whether to dereference symlinks
    wxFileName fn = wxFileName::DirName(path);
    if (!m_followLinks)
    {
        fn.DontFollowLink();
    }

    // TODO we know it is a dir, but it doesn't have to be
    if (m_filesList->GetItemText(idx).StartsWith("Dir:  "))
    {
        ret = m_watcher->Remove(fn);
    }
    else if (m_filesList->GetItemText(idx).StartsWith("Tree: "))
    {
        ret = m_watcher->RemoveTree(fn);
    }
    else
    {
        wxFAIL_MSG("Unexpected item in wxListView.");
    }

    if (!ret)
    {
        wxLogError("Error removing '%s' from watched paths", path);
    }
    else
    {
        m_filesList->DeleteItem(idx);
    }
}

void MyFrame::OnRemoveAll(wxCommandEvent& WXUNUSED(event))
{
    if ( !m_watcher->RemoveAll() )
    {
        wxLogError("Error removing all paths from watched paths");
    }

    m_filesList->DeleteAllItems();
}

void MyFrame::OnRemoveUpdateUI(wxUpdateUIEvent& event)
{
    event.Enable(m_filesList->GetFirstSelected() != wxNOT_FOUND);
}

void MyFrame::OnRemoveAllUpdateUI(wxUpdateUIEvent& event)
{
    event.Enable( m_filesList->GetItemCount() != 0 );
}

void MyFrame::OnFileSystemEvent(wxFileSystemWatcherEvent& event)
{
    // TODO remove when code is rock-solid
    wxLogTrace(wxTRACE_FSWATCHER, "*** %s ***", event.ToString());
    LogEvent(event);

    int type = event.GetChangeType();
    if ((type == wxFSW_EVENT_DELETE) || (type == wxFSW_EVENT_RENAME))
    {
        // If path is one of our watched dirs, we need to react to this
        // otherwise there'll be asserts if later we try to remove it
        wxString eventpath = event.GetPath().GetFullPath();
        bool found(false);
        for (size_t n = m_filesList->GetItemCount(); n > 0; --n)
        {
            wxString path;
            if ((!m_filesList->GetItemText(n-1).StartsWith("Dir:  ", &path)) &&
                (!m_filesList->GetItemText(n-1).StartsWith("Tree: ", &path)))
            {
                wxFAIL_MSG("Unexpected item in wxListView.");
            }
            if (path == eventpath)
            {
                if (type == wxFSW_EVENT_DELETE)
                {
                    m_filesList->DeleteItem(n-1);
                }
                else
                {
                    // At least in wxGTK, we'll never get here: renaming the top
                    // watched dir gives IN_MOVE_SELF and no new-name info.
                    // However I'll leave the code in case other platforms do
                    wxString newname = event.GetNewPath().GetFullPath();
                    if (newname.empty() ||
                        newname == event.GetPath().GetFullPath())
                    {
                        // Just in case either of these are possible...
                        wxLogTrace(wxTRACE_FSWATCHER,
                                   "Invalid attempt to rename to %s", newname);
                        return;
                    }
                    wxString prefix =
                        m_filesList->GetItemText(n-1).StartsWith("Dir:  ") ?
                                      "Dir:  " : "Tree: ";
                    m_filesList->SetItemText(n-1, prefix + newname);
                }
                found = true;
                // Don't break: a filepath may have been added more than once
            }
        }

        if (found)
        {
            wxString msg = wxString::Format(
                           "Your watched path %s has been deleted or renamed\n",
                           eventpath);
            m_evtConsole->AppendText(msg);
        }
    }
}


static wxString GetFSWEventChangeTypeName(int changeType)
{
    switch (changeType)
    {
    case wxFSW_EVENT_CREATE:
        return "CREATE";
    case wxFSW_EVENT_DELETE:
        return "DELETE";
    case wxFSW_EVENT_RENAME:
        return "RENAME";
    case wxFSW_EVENT_MODIFY:
        return "MODIFY";
    case wxFSW_EVENT_ACCESS:
        return "ACCESS";
    case wxFSW_EVENT_ATTRIB:  // Currently this is wxGTK-only
        return "ATTRIBUTE";
#ifdef wxHAS_INOTIFY
    case wxFSW_EVENT_UNMOUNT: // Currently this is wxGTK-only
        return "UNMOUNT";
#endif
    case wxFSW_EVENT_WARNING:
        return "WARNING";
    case wxFSW_EVENT_ERROR:
        return "ERROR";
    }

    return "INVALID_TYPE";
}

void MyFrame::LogEvent(const wxFileSystemWatcherEvent& event)
{
    wxString entry = wxString::Format(LOG_FORMAT + "\n",
                            GetFSWEventChangeTypeName(event.GetChangeType()),
                            event.GetPath().GetFullPath(),
                            event.GetNewPath().GetFullPath());
    m_evtConsole->AppendText(entry);
}
