//-------------------------------------------------------------------
// Projekt:   NDir, Nice Directory
// Datei:     dir.cpp -- Main program file
//------------------------------------------------------------------
// Autor:           Michael Weers
// Letzte Änderung: 2000-06-17
// Version:         0.8.3
// Rechner:         i386+, GNU Linux 
// Compiler:        GNU CC 2.91.66
//-------------------------------------------------------------------

#include <iostream>
#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>
#include <locale.h>
#include <vector>
#include <string>
#include <stdlib.h>

#ifdef __ultrix__
#include "Ultrix-Compatibility.hpp"
#endif

#include "DirectoryList.hpp"
#include "StringUtils.hpp"  // for icompare()
#include "ColorSetup.hpp"
#include "text/format.h"

using namespace std;

struct Options {
  bool show_info;
  bool show_locale_info;
  
  bool show_fileinfo;
  enum FileFilter_t { NON_HIDDEN, ALMOST_ALL, ALL };
  FileFilter_t filter;   // show hidden files?
  bool format_long; // long output format
  bool more_compact; // *Allow* view to do a more compact output than by default
  bool show_groups;
  
  enum ShowTime_t { STATUS_CHANGE, ACCESS, MODIFICATION };
  ShowTime_t show_time;
  bool dirs_as_files; // list directories as files instead of their contents
  bool recursive;
  // bool numeric_IDs= false;
  CompareFunction_t compareFunction;
  bool reverse;
  
  enum Colorization_t { NEVER, IF_TERMINAL, ALWAYS };
  Colorization_t colorization;
  
  bool utc; 

  Options(): show_info( false), 
             show_locale_info( false),
             show_fileinfo( false),
             filter( NON_HIDDEN),
             format_long( false), 
             more_compact( false),
             show_groups( false),
             show_time( MODIFICATION),
             dirs_as_files( false),
             recursive( false),
             compareFunction ( &NamePrecedence ),
             reverse( false),
             colorization( IF_TERMINAL),
             utc( false)
             
             {}
};


#include <sys/ioctl.h>

#include <unistd.h>

class Dir_view {
                  /** The stream the output is written to*/
              ostream& out;
                  /** The DirectoryList to be printed */
              DirectoryList* DL;
                  /** The options, mainly for output. Those options not affecting
                      the output are ignored of course */
      const   Options& options;
                  /** The color setup */
      const   ColorSetup& colors;
      
              int width;  // The terminal width in characters (0 if unspecified)
              bool is_terminal;
  
  public:
              Dir_view( DirectoryList* DL, const Options&, const ColorSetup&);
              
              int getWidth() { return width; }
              bool isTerminal() { return is_terminal; }
              void write();
  protected:
              int getLineWidth();

              void writeFileInfoList();
              void writeFileInfo( const UnixFile& );
              void writeListLong();
              void writeListWide();
              string longFormat( UnixFile& file);
  public:
      static  string StatisticsLine( long int, long int files, long long int size);
};


Dir_view::Dir_view( DirectoryList* DL, const Options& o, const ColorSetup& s) 
                 : out( cout), options( o), colors(s) {

  this->DL= DL;
  width= getLineWidth();
  is_terminal= (isatty( STDOUT_FILENO) != 0);
}

void Dir_view::write() {
  if ( !DL->directoryExists())
    out << "\t" << text::clean( DL->getDirectory().getCanonicalPath() )
         << ": No such file or directory." << endl;
  else if ( options.show_fileinfo){
    writeFileInfoList();
  }
  else {
    out << "\t" << "Directory: " + text::clean( DL->getDirectory().getCanonicalPath()) << "\n";
    if ( !DL->isReadable()) 
      out << "\t" << "Unable to read directory." << endl;
    else if ( DL->isEmpty() ) {
      out << "\t" << "No matching file found." << endl;
    }
    else {
      if (options.format_long) writeListLong();
      else                     writeListWide();

      out << "\t"; 
      out << StatisticsLine( DL->getAllFilesCount(),
                             DL->getRegFilesCount(),
                             DL->getRegFilesSize() ) << endl;
    } 
  }
}

int Dir_view::getLineWidth() {
// Attempt 1: Determine Terminal line width from terminal itself.
// Code taken from GNU ls
#ifdef TIOCGWINSZ
  {
    struct winsize ws;

    if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &ws) != -1 && ws.ws_col != 0)
      return ws.ws_col;
  }
#endif
// Attempt 2: Determine line width from Enviromnent variable
  char* value= getenv( "COLUMNS");
  if (value != NULL) {
    int i;
    int r= sscanf( value, "%d", &i);
    if (r == 1 && i > 0)  
      return i;
  }
// Attempt 3: default value
  return 80;
}

void Dir_view::writeFileInfoList() {
  out << endl;

  for ( vector<UnixFile*>::iterator pos= DL->file_list.begin();
        pos != DL->file_list.end();
        ++ pos ) 
  {  
    writeFileInfo( UnixFile( (**pos).getFullName(), UnixFile::AS_IS, true ) );
  }
}   
        

void Dir_view::writeFileInfo( const UnixFile& file) {
  out << "Name:                 " << text::clean( file.getName()) << endl
      << "Type:                 " ;

  switch (file.getType()) {
    case UnixFile::REGFILE:  out << "Regular file"; break;
    case UnixFile::DIRECTORY:  out << "Directory"; break;
    case UnixFile::SYMLINK: out << "Symbolic link";
             if ( file.isNavigable())  out << " to a directory";
             if ( file.isBrokenLink()) out << " (broken)";
             break;
    case UnixFile::BLOCK_DEVICE:  out << "Block Device"; break;
    case UnixFile::CHAR_DEVICE:  out << "Character Device"; break;
    case UnixFile::PIPE:  out << "Named Pipe"; break;
    case UnixFile::SOCKET:  out << "Socket";
  }
  out << endl
      << "Path:                 " << text::clean( file.getFullName()) << endl;

  if (file.isFile()) {
  out << "Size:                 " 
#ifdef NO_THOUSANDS_GROUPING
  << text::format("%lld",file.getSize()) 
#else
  << text::format("%'lld",file.getSize()) 
#endif
  << " bytes" << endl ;
  } 
  else if (file.isLink()) {
    string target;
    string link=file.getLinkName();
    if (link.size()>0 && link[0]=='/')  target= link;
    else target=  
      UnixFile::getCanonicalPath( UnixFile::getAbsolutePath(
           file.getParent() + UnixFile::separator + link ) );
    
    out << "Link target:          " << text::clean(target) << endl;
  }
  
  out   << "Type and permissions: " << file.getAttributeString() << endl
        << "Owner:                " << text::clean( file.getOwner( true) )
        << " (" << text::clean(file.getOwnerName()) << ")"
        << "   Group: " << text::clean(file.getGroup( true)) << endl
        << "Last modified:        " << file.getLastModified().toString( options.utc) << endl
        << "Last accessed:        " << file.getLastAccessed().toString( options.utc) << endl
        << "Last Status change:   " << file.getLastStatusChanged().toString( options.utc) << endl
        << endl;
}  

void Dir_view::writeListWide() {
  
  vector<UnixFile*>::iterator pos;
  
  int name_length= 0;          // find out longest file name
  for (pos= DL->file_list.begin(); pos != DL->file_list.end(); ++pos) {
    int l= (*pos)->getName().size();
    
    if (name_length < l) name_length= l;
  }  
  
  const int spacing= 3;
  
  int lineWidth= getLineWidth();          // determie terminal width...
                            
  int colWidth= name_length + spacing;
  int cols= lineWidth / colWidth;
  if (cols < 1)  cols= 1;
  int col= 0;               // # of current column (startig from 0)
    
  for (pos= DL->file_list.begin(); pos != DL->file_list.end(); ++pos) {
    UnixFile* f= (*pos); 
    if (f != NULL) {
      string::size_type entry_length= f->getName().size();

      out << colors.formatFilename( *f);
      if (f->isNavigable()) {
        out << '/';
        entry_length++;
      }
      
      if ( col >= cols-1) { // letzte Spalte
        out << endl;  
        col= 0;
      }
      else {
        string fill( colWidth - entry_length, ' ');
        out << fill;
        col++;
      }
    }
  } // for
  if (col != 0)  out << endl;
}
        

void Dir_view::writeListLong() {
    vector<UnixFile*>::iterator pos;

    for (pos= DL->file_list.begin(); pos != DL->file_list.end(); ++pos) {
      UnixFile* f= (*pos); 
      if (f != NULL) {
        out << longFormat( *f) << "\n";
      }
    }
}

string Dir_view::longFormat( UnixFile& file) {
  string s;  
  s.reserve( 160);
  s.append( file.getAttributeString()).append( options.more_compact ? " " : "  ");
  
  s += align( text::clean( file.getOwner( true)) , 9, LEFT);
  
  if (options.show_groups) {
    s += align( text::clean( file.getGroup( true)) , 9, LEFT);
  }
  
  Date date( 0);
  switch ( options.show_time) { 
    case Options::STATUS_CHANGE: date= file.getLastStatusChanged();  break;
    case Options::ACCESS: date= file.getLastAccessed();  break;
    default: date= file.getLastModified();  
  }
  s.append( date.toString( "%Y-%m-%d %H:%M", options.utc) );

  if ( file.isFile() ) {

  // format string: number: precision, ll: use long long int, 
  //                d: print integers as decimal numbers, ': use thousands separator
#ifdef NO_THOUSANDS_GROUPING
    s += text::format( "%16lld", file.getSize() ) + "  ";
#else
    s += text::format( "%'16lld", file.getSize() ) + "  ";
#endif
  }
  else if ( file.isNavigable() ) 
    if (file.isLink())                          // symlink to a directory 
      s += align( "[-> Dir]", 18, CENTER);
    else
      s += align( "[Dir]", 18, CENTER);
  else
    s.append( 18, ' ');
    
  // determine name string length
  string::size_type name_length= file.getName().size();
  if (file.isLink())  name_length += 4 + file.getLinkName().size(); // 4 chars " -> "
  
  // now: if we use a terminal and the name string is too long, put it 
  // in the next line, right justified.
  if (isTerminal() && s.size() + name_length > getWidth()) {
    s += "\n";
    // compiler warns about signed-unsigned-comp. but this is safe.
    // add space 
    if (getWidth() > name_length)  s.append( getWidth() - name_length, ' ');
  }
  s += colors.formatFilename( file);
  if (file.isLink())  s.append(" -> ").append( file.getLinkName());
  return s;
}

string Dir_view::StatisticsLine( long int all_files, long int files, long long int size) {
#ifdef NO_THOUSANDS_GROUPING
  string s = text::format( "%ld", all_files) + " entries,  "
    + text::format( "%ld", files ) + " regular files with " 
    + text::format( "%lld", size ) + " bytes total size";
#else
  string s = text::format( "%'ld", all_files) + " entries,  "
    + text::format( "%'ld", files ) + " regular files with " 
    + text::format( "%'lld", size ) + " bytes total size";
#endif
  return s;
}


class Dir_main {

  string progname;
  int argc;
  char** argv;

  vector<string> file_args;
  vector<DirectoryList*> L;  // List of directories

  Options options;
  ColorSetup colorsetup;
//  APrecedence* precedence;
  
  long long int summary_filesize;
  long int summary_filecount, summary_entrycount, summary_dirs_listed;

  public:
                   int returncode;
                   
                   Dir_main( int argc, char* argv[] );
                   ~Dir_main();
  
  protected:
              void handle_file_arg( const string& arg);

       /** Output a directory, eventually recurring into subdirectories */
              void handle_directory( DirectoryList& DL);

              void handle_argument( const string& arg);

  public:
              void run();
}; // class Dir_main


Dir_main::Dir_main( int argc, char* argv[] ):  returncode( 0) {
  progname= argv[0];
  this->argc= argc;
  this->argv= argv;
  summary_entrycount=0; summary_filecount=0; summary_filesize=0; summary_dirs_listed=0;
}


Dir_main::~Dir_main() {
  for( vector<DirectoryList*>::size_type i= 0; i < L.size(); ++i) {
     delete L[i];  L[i]= NULL;
  }  
}  


void Dir_main::handle_file_arg( const string& arg) {

  bool handeled= false;             // is f handeled anywhere?
  UnixFile* f= new UnixFile( arg);

  if ( !f->exists() || ( f->isNavigable() && !options.dirs_as_files )) {          
                          // create new DirectoryList and list it.

                          // ändern: nichtexistente Dateien schon
                          // hier fehlerbehandeln
    DirectoryList* DL= new DirectoryList( f );
    L.push_back( DL);
    handeled= true;
    return;
  }
  else { 
                          // f is an ordinary file (maybe a directory 
                          // if option 'dirs_as_files' is in effect); 
                          // search DirectoryList where it belongs to...
    vector<DirectoryList*>::iterator dir;
    for (dir= L.begin(); dir != L.end(); ++dir) {
      DirectoryList* DL = (*dir);
      UnixFile parent( f->getParent());
      if ( (*dir)->getDirectory().equals( parent) ) {  
                                    // ...found!
        DL->insert_unique( f);
        handeled= true;
        return;
      }
    }
    if (!handeled) {            // ... not found, make new DirectoryList
                                // appropriate to the file
      DirectoryList* DL= new DirectoryList( new UnixFile( f->getParent()), false );
      DL->insert( f);
      L.push_back( DL);
      handeled= true;
    }

  } // if-else
}

   /** Output a directory, eventually recurring into subdirectories */
void Dir_main::handle_directory( DirectoryList& DL) {
  DL.list( ! (options.filter == Options::NON_HIDDEN), options.filter == Options::ALL );    
  DL.syncStatistics();
  DL.setCompareFunction( options.compareFunction);
  DL.reversed_order= options.reverse;
  DL.ReOrder();                             // sort directory

  if (&DL != L.front())  cout << endl;      // A separating line between directories

  Dir_view v( &DL, options, colorsetup);
  v.write();
  
  // now for the overall statistics:
  if (DL.isReadable()) {
    summary_entrycount += DL.getAllFilesCount();
    summary_filecount += DL.getRegFilesCount();
    summary_filesize += DL.getRegFilesSize();
    summary_dirs_listed++;
  }

  if (options.recursive) {
    vector<UnixFile*>::iterator f;
    for ( f= DL.file_list.begin(); f != DL.file_list.end(); ++f) {

      if ( (*f)->isSubNavigable() ) {  // prevents recurring into links that
                                       // point to parent directories.
        DirectoryList DL_sub( new UnixFile( (**f).getPath()));
        DL_sub.setCompareFunction( options.compareFunction);

        handle_directory( DL_sub);
      }
    }
  }
}

void Dir_main::handle_argument( const string& arg) {  

  if (arg.size() > 0 && arg[0]=='-') {            // option
    if (arg=="-l")  options.format_long= true;
    else if (arg=="-a")  options.filter= Options::ALL;
    else if (arg=="-A")  options.filter= Options::ALMOST_ALL;
    else if (arg=="-?" || arg=="--help")  options.show_info= true;
    else if (arg=="-?l") options.show_locale_info= true;
    else if (arg=="-d")  options.dirs_as_files= true;
    else if (arg=="-g")  options.show_groups= options.more_compact= true;
    else if (arg=="-R")  options.recursive= true;
    else if (arg=="-t")  options.compareFunction= ModDatePrecedence;
    else if (arg=="-u")  options.show_time= Options::ACCESS;
    else if (arg=="-c")  options.show_time= Options::STATUS_CHANGE;
    else if (arg=="-S")  options.compareFunction= FileSizePrecedence;
    else if (arg=="-U")  options.compareFunction= 0;
    else if (arg=="-X")  options.compareFunction= ExtensionPrecedence;
    else if (arg=="-r")  options.reverse= true;
    else if (arg=="-cn")  options.colorization= Options::NEVER;
    else if (arg=="-ca")  options.colorization= Options::ALWAYS;
    else if (arg=="--info")  options.show_fileinfo= true;
    else if (arg=="--utc")  options.utc= true;
    // new options here ...
    else {
      cerr << "NDir:  Unrecognized option '" << arg << "'." << endl
           << "Aborting." << endl;
      returncode= 1;
      return;
    }
  }
  else
    file_args.push_back( arg);
}

void Dir_main::run() {
  // parsing arguments and set options

//   if ( progname.size() >= 2 && progname.compare( progname.size()-2, 2, "lv")==0 )
    if (progname.substr( 0, 2)=="lv") {
      options.format_long= true;
    }  

//   char* options_from_env= getenv("NDIR_OPTIONS");
//   if (options_from_env!=NULL) {
//     StringTokenizer st( options_from_env, " \t");
//     while (st.hasMoreTokens())  handle_option( st.getNextToken());
//   }    

  for ( int i=1; i < argc; ++i) {
    handle_argument( argv[i]);
    if (returncode!=0)  break;
  }
  // now "normalize" some options...
  
  if (options.compareFunction==ModDatePrecedence) {  // if -t was given,
    if (options.show_time==Options::ACCESS)      // and -s, then sort according to atime.
      options.compareFunction= AccessDatePrecedence;
    if (options.show_time==Options::STATUS_CHANGE)
      options.compareFunction= StatusChangeDatePrecedence;
  }
  if (options.show_fileinfo) {
    options.dirs_as_files= true;
  }
  switch (options.colorization) {
    case Options::NEVER:  colorsetup.do_colorization= false;  break;
    case Options::IF_TERMINAL:  colorsetup.do_colorization= (isatty( STDOUT_FILENO) != 0);  break;
    case Options::ALWAYS: colorsetup.do_colorization= true;  break;
  }
  
  for (vector<string>::iterator i= file_args.begin(); i != file_args.end(); ++i)
    handle_file_arg( *i);  

  
  // Now the program's main functions

  if (returncode!=0) {  // nothing...
  }
  else if (options.show_info) {
    cout << endl
      << "  NDir -- Nice Directory. Version 0.8.7" << endl
      << "  Copyright (c) 1997-2003 Michael Weers." << endl 
      << endl
      << "  Displays contents of directories." << endl
      << endl
      << "  Usage:  ndir / lw / lv  <Options> <Directories, Files>" << endl
      << "  lw and lv are alternative names for NDir. Use lv for a detailed listing." << endl
      << "  See the manual page (man ndir) for details." << endl 
      << endl;
  }
  else if (options.show_locale_info) {
    char* lc_collate= setlocale( LC_COLLATE, NULL);
    if (lc_collate != NULL)
      cout << "  NDir uses string comparison conventions for locale: "
           << lc_collate << endl
           << "  Latin characters are compared case-insensitive: "
           << ((icompare( "ax", "Ay") < 0) ? "Yes" : "No") << endl;
    char* lc_numeric= setlocale( LC_NUMERIC, NULL);
    if (lc_numeric != NULL)
      cout << "  NDir uses number display conventions for locale: "
           << lc_numeric << endl;
  
    cout << endl;
  }
  else {
      // now the usual -- print the directories
    if (L.empty()) {    // Special case: no DirectoryList created yet.
                            // create one with current directory
       handle_file_arg( ".");
    }
            // generate output.
            // note: recursive walk through directory tree is done within
            // handle_directroy()

    vector<DirectoryList*>::iterator d;
    for (d= L.begin(); d != L.end(); ++d) {
      handle_directory( **d );
    }

    if ( !options.show_fileinfo && (L.size() > 1 || options.recursive) ) {
       cout << endl 
            << colorsetup.format("Summary for "+text::format( "%ld", summary_dirs_listed)+" directories:", "01") << endl
            << "\t" << Dir_view::StatisticsLine( summary_entrycount, summary_filecount, summary_filesize) << endl;
    }
  }
} // run()
        

int main( int argc, char* argv[]) {
  setlocale( LC_ALL, "");                // i18n settings

  Dir_main application( argc, argv);
  application.run();
  return application.returncode;
}
