//=-- DetailedBreakout.cpp - Breakout for a "Details" page... ----------------=
//
//  PLUGIN DESCRIPTION:
//    This plugin is meant to operate on some sort of a "Details" page, where
//    it lists all (or at least most) of the accesses that have occured for 
//    the day.  As such, this plugin should only be used on relatively low
//    volume web pages.
//
//  PLUGIN PARAMETERS:
//    Filter:        A filter corresponding to the hits you want shown!
//    Mode:          Either "Flat" or "Breakout".  Default=Breakout
//    DominateKey:   The top-level key.  If set to "Host" (the default) the 
//                   plugin output lists the pages that each host visited.  If 
//                   set to "Page", then the plugin outputs the hosts that visit
//                   each page.
//    FoldReloads:   If "True" or "1", duplicate consecutive hits to a page 
//                   from a host are combined onto a single line.  Otherwise,
//                   list on seperate lines.  Default=True.
//    DepthLimit:    Limit the maximum URL depth to a value. Default=0 (disable)
//    MaxKeys:       Limit the number of dominate keys to display... 
//    MaxKeyEntries: Limit the number of entries per dominate key.
//
//  PLUGIN AUTHOR:
//    Chris Lattner, Author of Magicstats  (sabre@nondot.org)
//
//  PLUGIN HOMEPAGE:
//     http://www.nondot.org/MagicStats/Plugins/DetailedBreakout/
//
//  PLUGIN VERSION / COMPATIBILITY:
//     DetailedBreakout v1.0 / MagicStats 2.0+
//
//=---------------------------------------------------------------------------=
//  This file is Copyright (c) 2000 Chris Lattner and stuff.
//=---------------------------------------------------------------------------=

#include "PagePlugin.h"
#include "SerializeEx.h"
#include "Table.h"
#include "DetailedBreakout.h"

INIT_PLUGIN(DetailedBreakout);

DetailedBreakout::DetailedBreakout(int &CE) { 
  Flags = FAutoFilter | FIgnoreErrors;
  DominateKey = AccessFormatPlugin::FEHost;
  SecondaryKey = AccessFormatPlugin::FEURL;
  ResetState();
  CE = 0;
}

// The full name consists of all of the data settings that alter the data that
// is kept track of and stored.
void DetailedBreakout::GetFullName(VarTable &Params, String &fullName, 
                                 int updateFreq) {
  String Filt;
  if (Params["Filter"] != 0) {
    Filt = Params["Filter"]->GetStringValue();
  } else {
    Filt = DefFilter.ToParamStr();
  }

  fullName = String::IntToStr(updateFreq) + Name + Filt;
}

void DetailedBreakout::ProcessAccess(AccessFormatPlugin &A) {
  const String &Key = A.GetField(DominateKey);
  const String &Sec = A.GetField(SecondaryKey);
  const String &Ref = A.GetReferrer();
  
  // Keep track of the key...
  KeyRec &KeyRecord = DataTable[Key];
  KeyRecord.AddToTail(HitRec(A.GetDate(), HitInfo(Sec, Ref)));
}

void DetailedBreakout::ResetState() {
  DataTable.Clear();        // Free all of the data in the linked list...
  DataTable.SetDefault(KeyRec());
}

void DetailedBreakout::AddExplodedDirectories(String Path, String &T, 
                                              int Depth,
                                              BreakoutRenderer::Tree &TheTree) {
  String CurDir;
  LinkedList<String> Fields;

  while (T.Length()) {
    int Count = T.strchr('/');
    if (Count == -1) break;  // Shouldn't happen with URLs... garbage?

    CurDir = T; CurDir.Left(Count+1);
    T.Right(T.Length()-Count-1);
    Path += CurDir;

    //    cout << "Path = '" << Path << "' CD = '" << CurDir << "'\n";

    if (Path == String("http://")) continue;
    if (Path == String("http:/")) continue;

    if (CurDir.Length() > 1)
      CurDir.Left(Count);  // Strip off the trailing /...

    Fields.AddToTail(String("<a href='") + Path + "'>" + CurDir + "</a>");
    TheTree.AddToTail(DataPair<int,LinkedList<String> >(Depth, Fields));

    Fields.RemoveHead();
    Depth++;
  }
}

void DetailedBreakout::OutputHTML(ostream &O, VarTable &Params) {
  int CutOff = 3, Percent = 1;   // Default to 3%
  
  int ModeOutLine = 1;
  int FoldReloads = 1;
  int ShowReferrer = 1;

  if (Params.InTable("Mode")) {
    ModeOutLine = CSString(Params["Mode"]->GetStringValue()) ==
                                CSString("Outline");
  }

  if (Params.InTable("FoldReloads")) 
    FoldReloads = VTExp::IsTrue(Params["FoldReloads"]);
  if (Params.InTable("ShowReferrer"))
    ShowReferrer = VTExp::IsTrue(Params["ShowReferrer"]);

  if (Params.InTable("CutoffValue")) {
    String Val = Params["CutoffValue"]->GetStringValue();
    if (Val.Length() > 0) {
      CutOff = Val.atoi();

      if (Val[Val.Length()-1] != '%')   // Not Percentage?
        Percent = 0;
    }
  }

  LinkedList<DataPair<String, KeyRec> > DataList;
  DataTable.ConvertToList(DataList);

  if (DataList.GetLength() == 0) {
    O << "<H1>No hits yet...</H1>\n";
  }

  // Sort data by primary key...
  DataList.Sort();

  if (!ModeOutLine || DominateKey == AccessFormatPlugin::FEURL) {
    LinkedList<DataPair<String, KeyRec> >::Iterator I = DataList.GetIterator();

    O << "<table border=0>";

    // Loop over each primary key...
    for (; I; I++) {
      String DateStr;
      O << "<TR><TD colspan=3><br><a name='" << I->P << "'><font size=+1><b>"
        << I->P << "</b></font></td></tr>";

      // Iterate over the list of hits now...
      KeyRec::Iterator J = I->S.GetIterator();
      
      for (; J; J++) {
        if (UpdateFreq == Plugin::UpdateDaily)
          J->P.strftime(DateStr, "%r");
        else
          J->P.strftime(DateStr, "%r %m/%d/%Y");


        O << "<td width=30> </td><td><a href='" << J->S.P << "'>" << J->S.P
          << "</a></td><td align=right>" << DateStr 
          << "</td><td align=left>" << J->S.S << "</td></tr>\n";
      }
    }
    O << "</table>\n";

  } else {
    BreakoutRenderer Renderer(O, Params, Descriptor);

    LinkedList<DataPair<String, KeyRec> >::Iterator I = DataList.GetIterator();

    // Loop over each primary key...
    LinkedList<String> Caption;
    Caption.AddToTail(""); Caption.AddToTail("Time:"); 

    if (ShowReferrer)
      Caption.AddToTail("Referrer:");

    LinkedList<String> ColumnSettings;
    ColumnSettings.AddToTail("");  // Leave the breakout part alone
    ColumnSettings.AddToTail("align=center"); // Center the date
    ColumnSettings.AddToTail("align=left");  // Left align the referrer

    for (; I; I++) {
      String DateStr;
      String OldPath = "";
      String OldTopDir = "";
      int OldDepth = 0;

      Caption.RemoveHead();  // Replace host name...
      Caption.AddToHead(String("<a name='") + I->P + "'>" + I->P + ":");
      BreakoutRenderer::Tree TheTree;

      // Iterate over the list of hits now...
      KeyRec::Iterator J = I->S.GetIterator();
      
      LinkedList<String> Fields;
      for (; J; J++) {
        String &URL = J->S.P;
        int SlashIdx = URL.strrchr('/');
        String TopDir = URL;  TopDir.Right(TopDir.Length()-SlashIdx-1);
        String Path   = URL;  Path.Left(SlashIdx+1);
        if (URL.Length() == 1) TopDir = '/';  // Don't nuke root

        int Depth = BreakoutRenderer::CalculateURLDepth(URL);

        // Did we change directories?  If so, we may have to put connector
        // directories into the tree (without times) to make the path 
        // contiguous... otherwise there could be holes!!!
        //
        if (Path != OldPath) {
          //cout << "Path = " << Path << " OldPath = " << OldPath << endl;

          if (Path.nCompare(OldPath) != 0) { // Is one a substr of the other?
            // Path doesn't starts with OldPath... keep choping off of OldPath
            // a directory at a time until Path and OldPath are substrings of
            // each other.
            //
            int IDX = OldPath.strrchr('/', OldPath.Length()-2);
            while (IDX != -1 && Path.nCompare(OldPath) != 0) {
              OldPath.Left(IDX+1);
              OldDepth--;
              IDX = IDX > 0 ? OldPath.strrchr('/', IDX-1) : -1;
            }
          }

          if (OldDepth < Depth) { // Check to see if we just went deeper
            // Okay we just went down a directory or two or three...
            // fill in the holes if there would be any created here...
            //
            String T = Path;
            T.Right(T.Length() - OldPath.Length());
            if (T != OldTopDir + '/') 
              AddExplodedDirectories(OldPath, T, OldDepth, TheTree); 
          }
                   
          OldPath = Path;
          OldDepth = Depth;
        }

        if (UpdateFreq == Plugin::UpdateDaily)
          J->P.strftime(DateStr, "%r");
        else
          J->P.strftime(DateStr, "%r %m/%d/%Y");
        DateStr.ReplaceSubString(" ", "&nbsp;");

        String Referrer(String("<a href='") + J->S.S + "'>" + 
                        J->S.S + "</a>");
        if (J->S.S.Length() == 0)
          Referrer = "";

        Fields.ClearList();
        Fields.AddToTail(String("<a href='") + URL + "'>" + TopDir + "</a>");
        Fields.AddToTail(DateStr);
        if (ShowReferrer)
          Fields.AddToTail(Referrer);
        TheTree.AddToTail(DataPair<int,LinkedList<String> >(Depth, Fields));
        OldTopDir = TopDir;
      }

      // Write it all out now!
      Renderer.OutputBreakout(Caption, TheTree, ColumnSettings);
      O << "<p>\n";
    }
  }
}

void DetailedBreakout::LoadState(Serialize &I) {
  unsigned int Version = 0;
  String Page;
  
  ResetState();
  I >> Version;
  
  // Check version number...
  switch (Version) {
  case 101:                  // Version 1.01
    I >> DataTable;
    break;
    
  case 100:                  // Version 1.00 - No referrer information!
    {
      typedef DataPair<Date, String> OHitRec;
      typedef LinkedList<OHitRec>     OKeyRec;
      Table<String, OKeyRec> ODataTable;

      I >> ODataTable;  // Read the old format...

      DataTable.Clear();

      // Convert data to new format...
      Table<String, OKeyRec>::Iterator TI = ODataTable.GetStart();
      for (; TI; TI++) {
        const OKeyRec &KR = TI.GetValue();
        LinkedList<HitRec> NewList;

        OKeyRec::Iterator LI = KR.GetIterator();
        for (; LI; LI++) {
          const OHitRec &Rec = *LI;
          // Set referrer to ""
          NewList.AddToTail(HitRec(Rec.P, HitInfo(Rec.S, "")));
        }

        DataTable.SetElementTo(TI.GetKey(), NewList);
      }
    }
    break;

  default:
    cout << "Serialized " << Name << " Page plugin is of a version that cannot "
         << "be deserialized\nwith this version of the plugin.  Reseting state.\n";
    break;
  }
  
}

void DetailedBreakout::SaveState(Serialize &O) {
  O << GetCurrentVersion();  // Serialize the Version number
  O << DataTable;
}
