// Text File Viewer mit Such-Index
// Variante mit Combo-Box
//
// Aufruf: viewer-index
//
// Klaus Kusche

#include <wx/app.h>
#include <wx/frame.h>
#include <wx/splitter.h> 
#include <wx/textctrl.h> 
#include <wx/dirctrl.h> 
#include <wx/button.h> 
#include <wx/combobox.h> 
#include <wx/sizer.h> 
#include <wx/event.h>

#include <map>
#include <vector>

// das ist cctype für wide chars (16 bit Unicode)
#include <cwctype>

using namespace std;

// unser Index ordnet jedem Wort eine Positionsliste zu (das macht eine map)
// die Positionsliste ist ein vector von long-Werten
// (die long's sind Zeile und Spalte im Text zusammencodiert)
typedef vector<long> poslist_t;
typedef map<wxString,poslist_t> index_t;

class MyApp: public wxApp
{
  virtual bool OnInit();
};

IMPLEMENT_APP(MyApp)

class MyFrame: public wxFrame
{
  public:
    MyFrame(const wxString &title);

    void OnSel(wxTreeEvent &event);
    void OnQuit(wxCommandEvent &event);
    void OnLeft(wxCommandEvent &event);
    void OnRight(wxCommandEvent &event);
    void OnBoth(wxCommandEvent &event);
    void OnUpdate(wxCommandEvent &event);
    void OnPrev(wxCommandEvent &event);
    void OnNext(wxCommandEvent &event);
        
  private:
    void BuildIndex();
    void Deselect();
    
    wxSplitterWindow *Splitter;
    wxGenericDirCtrl *Tree;
    wxTextCtrl *Text;
    // wir brauchen das Suchfeld und die Such-Buttons in den Event-Handlern,
    // daher müssen sie Member sein
    wxComboBox *Search;
    wxButton *Prev;
    wxButton *Next;

    index_t index;         // Such-Index
    // zum einfacheren Merken und Markieren der aktuellen Fundstelle
    poslist_t *cur_list;   // Pointer auf Pos-Liste des aktuellen Wortes oder nullptr
    int cur_index;         // aktueller Index (gearde markiert) in cur_list
    int cur_len;           // Länge des aktuellen Wortes
};

// Nummern für die Buttons zur Unterscheidung ihrer Klick-Events
// und auch für die Textfelder
enum
{
  ID_Quit = 1,
  ID_Left,
  ID_Right,
  ID_Both,
  ID_Prev,
  ID_Next,
  ID_MainText,
  ID_Search
};

bool MyApp::OnInit()
{
  MyFrame *frame = new MyFrame("File Viewer");
  frame->Show(true);
  SetTopWindow(frame);
  return true;
} 

MyFrame::MyFrame(const wxString &title)
: wxFrame(nullptr, -1, title),
  Splitter(new wxSplitterWindow(this, -1)),
  Tree(new wxGenericDirCtrl(Splitter, -1)),
  Text(new wxTextCtrl(Splitter, ID_MainText, "", 
       wxDefaultPosition, wxDefaultSize, 
       wxTE_MULTILINE | wxTE_READONLY | wxTE_DONTWRAP | wxTE_PROCESS_TAB | wxTE_NOHIDESEL)),
  Search(new wxComboBox(this, ID_Search, "",
         wxDefaultPosition, wxDefaultSize,
         0, nullptr, wxCB_DROPDOWN | wxTE_PROCESS_ENTER)),
  Prev(new wxButton(this, ID_Prev, "&Prev")),
  Next(new wxButton(this, ID_Next, "&Next")),
  index(), cur_list(nullptr), cur_index(0), cur_len(0)
{
  // neue Buttons und Sizer anlegen
  // da wir sie in den Event-Methoden nicht brauchen,
  // reichen dafür lokale Variablen (Member sind auch ok, aber unnötig)
  // das "&" in der Button-Beschriftung markiert den Shortkey
  wxButton *Quit = new wxButton(this, ID_Quit, "E&xit");
  wxButton *Left = new wxButton(this, ID_Left, "&Left side");
  wxButton *Right = new wxButton(this, ID_Right, "&Right side");
  wxButton *Both = new wxButton(this, ID_Both, "&Both sides");
  // das Argument (eine int-Konstante) gibt die "Stapelrichtung" an
  wxBoxSizer *mainsizer = new wxBoxSizer(wxVERTICAL);
  wxBoxSizer *buttonsizer = new wxBoxSizer(wxHORIZONTAL);
  wxBoxSizer *searchsizer = new wxBoxSizer(wxHORIZONTAL);

  CreateStatusBar();
  Prev->Disable();
  Next->Disable();

  // 1. Arg: Objekt
  // 2. Arg: Proportionaler Anteil an der Größe (0: wächst nicht mit)
  // 3. Arg: Optionen.
  // wxALL: Rundherum Rand lassen (wxTOP: nur oben)
  // wxEXPAND: In beide Richtungen den vorhandenen Platz ausfüllen
  // 4. Arg: Randbreite in Pixel
  searchsizer->Add(Search, 2, wxALL | wxEXPAND, 5);    
  searchsizer->Add(Prev, 1, wxALL | wxEXPAND, 5);    
  searchsizer->Add(Next, 1, wxALL | wxEXPAND, 5);    
  buttonsizer->Add(Left, 1, wxALL | wxEXPAND, 5);    
  buttonsizer->Add(Both, 1, wxALL | wxEXPAND, 5);    
  buttonsizer->Add(Right, 1, wxALL | wxEXPAND, 5);    
  buttonsizer->Add(Quit, 1, wxALL | wxEXPAND, 5);    
  mainsizer->Add(Splitter, 1, wxTOP | wxEXPAND, 5);
  mainsizer->Add(searchsizer, 0, wxEXPAND);
  mainsizer->Add(buttonsizer, 0, wxEXPAND);
  mainsizer->SetMinSize(800, 600);  // minimale Größe festlegen
  // "mainsizer ist der Sizer des Hauptfensters":
  // Größen von Fenster und mainsizer koppeln
  SetSizerAndFit(mainsizer);
  Splitter->SetMinimumPaneSize(20);
  Splitter->SplitVertically(Tree, Text, 250);
  
  Tree->SetPath(wxGetCwd());

  Bind(wxEVT_COMMAND_TREE_ITEM_ACTIVATED, &MyFrame::OnSel, this);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnQuit, this, ID_Quit);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnLeft, this, ID_Left);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnRight, this, ID_Right);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnBoth, this, ID_Both);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnPrev, this, ID_Prev);
  Bind(wxEVT_COMMAND_BUTTON_CLICKED, &MyFrame::OnNext, this, ID_Next);
  Bind(wxEVT_COMMAND_TEXT_UPDATED, &MyFrame::OnUpdate, this, ID_Search);
  Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, &MyFrame::OnUpdate, this, ID_Search);
}

// Wird aufgerufen wenn Exit-Button gedrückt wurde
void MyFrame::OnQuit(wxCommandEvent &WXUNUSED(event))
{
  Close(true);  // Hauptfenster schließen
}

// Umschalten der Ansicht
void MyFrame::OnLeft(wxCommandEvent &WXUNUSED(event))
{
  if (Splitter->IsSplit()) 
    Splitter->Unsplit(Text);
  else if (Splitter->GetWindow1() == Text) {
    Splitter->ReplaceWindow(Text,Tree);
    Text->Hide();
    Tree->Show();
  }
}

void MyFrame::OnRight(wxCommandEvent &WXUNUSED(event))
{
  if (Splitter->IsSplit()) 
    Splitter->Unsplit(Tree);
  else if (Splitter->GetWindow1() == Tree) {
    Splitter->ReplaceWindow(Tree,Text);
    Tree->Hide();
    Text->Show();
  }
}

void MyFrame::OnBoth(wxCommandEvent &WXUNUSED(event))
{
  if (!Splitter->IsSplit()) {
    Splitter->SplitVertically(Tree, Text, 250);
    Tree->Show();
    Text->Show();
  }
}

void MyFrame::OnSel(wxTreeEvent &WXUNUSED(event))
{
  Deselect();
  Text->LoadFile(Tree->GetFilePath());
  BuildIndex();
}

void MyFrame::OnPrev(wxCommandEvent &WXUNUSED(event))
{
  --cur_index;
  long pos = (*cur_list)[cur_index];
  if (cur_index == 0) Prev->Disable();
  Next->Enable();
  Text->ShowPosition(pos);   // scrollen, sodass Stelle pos sichtbar ist
  Text->SetSelection(pos, pos + cur_len);
}

void MyFrame::OnNext(wxCommandEvent &WXUNUSED(event))
{
  ++cur_index;
  long pos = (*cur_list)[cur_index];
  if (cur_index == cur_list->size() - 1) Next->Disable();
  Prev->Enable();
  Text->ShowPosition(pos);
  Text->SetSelection(pos, pos + cur_len);
}

void MyFrame::OnUpdate(wxCommandEvent &WXUNUSED(event))
{
  Deselect();

  // Ersetzt durch "auto"
  //index_t::iterator wordPos;

  // Seit C++ 11:
  // Bei "auto" als Typ bekommt die Variable automatisch den Typ,
  // den der initialisierungswert hat
  auto wordPos = index.find(Search->GetValue());
  
  if (wordPos == index.end()) {
    SetStatusText("Not Found");
  } else {
    cur_list = &(wordPos->second);
    cur_index = 0;
    cur_len = Search->GetValue().Len();
    
    long pos = (*cur_list)[0];
    Text->ShowPosition(pos);
    Text->SetSelection(pos, pos + cur_len);
    
    if (cur_list->size() > 1) Next->Enable();

    wxString info;
    info << cur_list->size() << " matches";
    SetStatusText(info);
  }
}

void MyFrame::Deselect()
{
  if (cur_list != nullptr) {
    cur_list = nullptr;
    Prev->Disable();
    Next->Disable();
    SetStatusText("");
    Text->SetSelection(0, 0);
  }
}

void MyFrame::BuildIndex()
{
  index.clear();

  int lines = Text->GetNumberOfLines();
  for (int lnr = 0; lnr < lines; ++lnr) {
    wxString line = Text->GetLineText(lnr);
    for (int col = 0; line[col] != '\0'; ) {
      if (iswalpha(line[col])) {
        // Wort-Anfang: Wort-Ende finden, Wort ausschneiden und speichern
        int len;
        for (len = 1; iswalpha(line[col + len]); ++len) {}
        wxString word = line.Mid(col, len);
        long wordPos = Text->XYToPosition(col, lnr);
        // index[word] liefert das second (die Posliste)
        // des map-Eintrags von word
        // wenn es noch keinen map-Eintrag für word gibt,
        // wird automatisch ein neuer, leerer Eintrag angelegt
        // push_back hängt die aktuelle Pos an die Posliste an
        index[word].push_back(wordPos);
        col += len;   // Wort überspringen: Hinter Wort weitermachen
      } else {
        // kein Wort, nächstes Zeichen anschauen
        ++col;
      }
    }
  }

  // ganze map durchlaufen, jedes Wort in die Combo-Box übernehmen
  Search->Clear();
  // alte Variante mit Iterator  
  //index_t::iterator wordPos;
  //for (wordPos = index.begin(); wordPos != index.end(); ++wordPos) {
  //  Search->Append(wordPos->first);
  //}
  // C++ 11 Variante mit "for :"-Schleife und auto
  for (auto &i : index) {
    Search->Append(i.first);
  }
}
