/*  GNU Moe - My Own Editor
    Copyright (C) 2005, 2006, 2007, 2008, 2009 Antonio Diaz Diaz.

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include <algorithm>
#include <cctype>
#include <cerrno>
#include <string>
#include <vector>
#include <dirent.h>
#include <sys/stat.h>

#include "buffer.h"
#include "buffer_handle.h"
#include "iso_8859.h"
#include "menu.h"
#include "rc.h"
#include "regex.h"
#include "screen.h"


namespace Menu {

void show( const std::vector< std::string > & name_vector, const int lines,
           const int columns, const int current, Point & cursor,
           const bool indicator = false ) throw()
  {
  static int top_line = 0;
  const int entries = name_vector.size();
  const int bottom_line = ( entries - 1 ) / columns;
  if( bottom_line - top_line + 1 < lines )
    top_line = std::max( 0, bottom_line - lines + 1 );
  const int current_line = current / columns;
  if( current_line < top_line ) top_line = current_line;
  if( current_line >= top_line + lines ) top_line = current_line - lines + 1;
  const int first_entry = top_line * columns;

  for( int line = 0; line < lines; ++line )
    {
    int i1 = -1, i2 = -1, i_ini = first_entry + ( line * columns );
    std::string s;
    for( int i = i_ini; i < i_ini + columns && i < entries; ++i )
      {
      if( i == current )		// highlighted entry
        {
        i1 = s.size();
        if( indicator ) ++i1;		// skip modified indicator
        s += name_vector[i];
        i2 = s.size();
        cursor = Point( line, std::min( i2, Screen::width() - 1 ) );
        }
      else s += name_vector[i];
      const unsigned int col = std::min( Screen::width(), ( ( ( i % columns ) + 1 ) * ( Screen::width() + 2 ) ) / columns );
      if( (int)col < Screen::width() && s.size() > col - 2 ) s.resize( col - 2 );
      while( s.size() < col ) s += ' ';
      }
    Screen::out_menu_line( s, Screen::height() - lines + line - 1, i1, i2 );
    }
  }


void append_common_prefix( std::string & name,
                           const std::vector< std::string > & name_vector ) throw()
  {
  for( unsigned int i = 0; i < name_vector[0].size(); ++i )
    {
    const char ch = name_vector[0][i];
    for( unsigned int j = 1; j < name_vector.size(); ++j )
      if( i >= name_vector[j].size() || ch != name_vector[j][i] )
        return;
    name += ch;
    }
  }


bool append_slash_if_dir( std::string & name ) throw()
  {
  if( name.size() && name[name.size()-1] != '/' )
    {
    struct stat st;
    if( !stat( name.c_str(), &st ) && S_ISDIR( st.st_mode ) )
      { name += '/'; return true; }
    }
  return false;
  }


void append_part_or_go_up( std::string & name, const std::string & new_part ) throw()
  {
  if( new_part == ".." )
    {
    if( name == "/" ) return;
    if( name == "//" ||
        ( name.size() >= 4 && name.compare( name.size() - 4, 4, "/../" ) ) ||
        ( name.size() == 3 && name != "../" ) )
      {
      int i = name.size() - 1; while( i > 0 && name[i-1] != '/' ) --i;
      name.resize( i );
      return;
      }
    }
  name += new_part; append_slash_if_dir( name );
  }


void split_filename( const std::string & name,
                     std::string & dirname, std::string & basename ) throw()
  {
  unsigned int first = 0;		// first character of basename

  for( unsigned int i = name.size(); i > 0; --i )
    if( name[i-1] == '/' ) { first = i; break; }

  if( first > 0 ) dirname.assign( name, 0, first );
  else dirname.clear();
  if( first < name.size() )
    basename.assign( name, first, name.size() );
  else basename.clear();
  }


bool valid_filename( const std::string & name ) throw()
  {
  const std::string & basename = my_basename( name );
  return ( basename.size() && basename != "." && basename != ".." );
  }

} // end namespace Menu


// Returns handle index, or -1 if aborted.
//
int Menu::bufhandle_menu( int current, const int abort_key )
  {
  std::vector< std::string > name_vector;

  const int entries = Bufhandle_vector::handles();
  for( int i = 0; i < entries; ++i )
    {
    const int c = Bufhandle_vector::circular_index( i );
    const Buffer & buffer = Bufhandle_vector::handle( c ).buffer();
    std::string tmp( " " ); tmp += buffer.uname();
    if( buffer.modified() ) tmp[0] = '*';
    name_vector.push_back( tmp );
    }

  if( entries < 2 || entries != (int)name_vector.size() ) return -1;

  int max_width = 0;
  for( int i = 0; i < entries; ++i )
    if( (int)name_vector[i].size() > max_width )
      max_width = name_vector[i].size();
  if( max_width == 0 ) return -1;

  const int columns = std::max( 1, ( Screen::width() + 2 ) / ( max_width + 2 ) );
  const int _height = ( entries + columns - 1 ) / columns;

  const int lines = std::min( 10, _height );
  const int line = Screen::height() - lines - 1;
  Point cursor( 0, Screen::width() - 1 );

  Screen::save_lines_and_cursor( line, lines + 1 );

  Screen::out_line( "Select buffer handle or 0-9 (^C to abort)", Screen::height() - 1 );
  current = Bufhandle_vector::absolute_index( current );
  int key = 0;
  while( key >= 0 )
    {
    show( name_vector, lines, columns, current, cursor, true );
    Screen::move_to( Point( line, 0 ) + cursor );
    bool valid_key = false;
    while( !valid_key )
      {
      valid_key = true;
      do key = Screen::wait_kbhit( line ); while( key < 0 );
      if( key == abort_key ) key = 3;
      if( std::isdigit( key ) )
        current = ( ( entries - 1 ) * ( key - '0' ) ) / 9;
      else switch( key )
        {
        case 3   : key = -1; break;		// ^C
        case ' ' :
        case '\r': key = -2; break;
        default: switch( Screen::convert_key( key ) )
                   {
                   case Screen::key_up   :
                     if( current >= columns ) current -= columns; break;
                   case Screen::key_down :
                     if( current + columns < entries ) current += columns; break;
                   case Screen::key_left : if( current > 0 ) --current; break;
                   case Screen::key_right: if( current < entries - 1 ) ++current; break;
                   case Screen::key_ppage:
                     for( int i = 0; i < lines && current >= columns; ++i )
                       current -= columns;
                     break;
                   case Screen::key_npage:
                     for( int i = 0; i < lines && current + columns < entries; ++i )
                       current += columns;
                     break;
                   case Screen::key_home : current = 0; break;
                   case Screen::key_end  : current = entries - 1; break;
                   default: valid_key = false;
                   }
        }
      }
    }
  Screen::restore_lines_and_cursor();
  if( key == -1 ) return -1;
  else return Bufhandle_vector::circular_index( current );
  }


// 'name' contains the filename to be modified (may be empty).
// Returns false if aborted.
//
bool Menu::file_menu( const std::string & prompt, std::string & name )
  {
  std::string dirname, basename;
  std::vector< std::string > name_vector;

  tilde_expansion( name );
  split_filename( name, dirname, basename );
  basename += '*';

  DIR * dirp = opendir ( dirname.size() ? dirname.c_str() : "./" );
  if( !dirp ) return true;
  while( true )
    {
    const struct dirent * entryp = readdir( dirp );
    if( !entryp ) { closedir( dirp ); break; }
    std::string tmp( entryp->d_name );
    if( tmp != "." && Regex::match_filename( basename, tmp ) )
      name_vector.push_back( tmp );
    }
  if( name_vector.size() > 1 )
    { name = dirname; append_common_prefix( name, name_vector ); }

  const int entries = name_vector.size();
  if( !entries ) return true;
  if( entries > 1 ) std::sort( name_vector.begin(), name_vector.end() );

  if( entries == 1 || ( entries == 2 && name_vector[0] == ".." ) )
    { name = dirname + name_vector.back(); append_slash_if_dir( name );
    return true; }

  int max_width = 0;
  for( int i = 0; i < entries; ++i )
    if( (int)name_vector[i].size() > max_width )
      max_width = name_vector[i].size();
  if( max_width == 0 ) return true;

  const int columns = std::max( 1, ( Screen::width() + 2 ) / ( max_width + 2 ) );
  const int _height = ( entries + columns - 1 ) / columns;

  const int lines = std::min( 10, _height );
  const int line = Screen::height() - lines - 1;
  Point cursor( 0, Screen::width() - 1 );

  Screen::save_lines_and_cursor( line, lines + 1 );

  int current = 0, key = 0;
  while( key >= 0 )
    {
    show( name_vector, lines, columns, current, cursor );
    Screen::out_line( prompt + name, Screen::height() - 1, true );
    Screen::move_to( Point( line, 0 ) + cursor );
    bool valid_key = false;
    while( !valid_key )
      {
      valid_key = true;
      do key = Screen::wait_kbhit( line ); while( key < 0 );
      switch( key )
        {
        case 3   : key = -1; break;		// ^C
        case '\t': key = -2; break;
        case ' ' :
        case '\r': key = -3; name = dirname;
                   append_part_or_go_up( name, name_vector[current] );
                   break;
        default: if( key < 256 && ISO_8859::isprint( key ) )
                   { name += key; key = -4; }
                 else switch( Screen::convert_key( key ) )
                   {
                   case Screen::key_up   :
                     if( current >= columns ) current -= columns; break;
                   case Screen::key_down :
                     if( current + columns < entries ) current += columns; break;
                   case Screen::key_left : if( current > 0 ) --current; break;
                   case Screen::key_right: if( current < entries - 1 ) ++current; break;
                   case Screen::key_ppage:
                     for( int i = 0; i < lines && current >= columns; ++i )
                       current -= columns;
                     break;
                   case Screen::key_npage:
                     for( int i = 0; i < lines && current + columns < entries; ++i )
                       current += columns;
                     break;
                   case Screen::key_home : current = 0; break;
                   case Screen::key_end  : current = entries - 1; break;
                   default: valid_key = false;
                   }
        }
      }
    }
  Screen::restore_lines_and_cursor();
  return ( key != -1 );
  }


// 'name' contains the default result (may be empty), and
// stores the result (if any).
// Returns the size of the result string, 0 if none, -1 if aborted.
// 'name' is cleared if aborted.
//
int Menu::get_filename( const std::string & prompt, std::string & name,
                             const int abort_key )
  {
  static std::vector< std::string > file_history;

  file_history.push_back( name );
  int len = Screen::get_string( prompt, file_history, abort_key, true, true );
  if( len > 0 ) name = file_history.back();
  else name.clear();
  return len;
  }


bool Menu::is_regular( const std::string & name, const bool quiet ) throw()
  {
  if( name.size() && name[name.size()-1] != '/' )
    {
    struct stat st;
    const int i = stat( name.c_str(), &st );
    if( ( i == 0 && S_ISREG( st.st_mode ) ) ||
        ( i == -1 && errno == ENOENT ) ) return true;
    if( !quiet ) Screen::show_message( "Not a regular file" );
    }
  return false;
  }


bool Menu::make_path( const std::string & name ) throw()
  {
  std::string dirname, basename;
  split_filename( name, dirname, basename );
  if( !basename.size() || basename == "." || basename == ".." ) return false;

  unsigned int index = 0;
  while( index < dirname.size() )
    {
    while( index < dirname.size() && dirname[index] == '/' ) ++index;
    unsigned int first = index;
    while( index < dirname.size() && dirname[index] != '/' ) ++index;
    if( first < index )
      {
      std::string partial( dirname, 0, index );
      struct stat st;
      if( !stat( partial.c_str(), &st ) )
        { if( !S_ISDIR( st.st_mode ) ) return false; }
      else if( mkdir( partial.c_str(), S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH ) )
        return false;
      }
    }
  return true;
  }


const std::string & Menu::my_basename( const std::string & name ) throw()
  {
  static std::string basename;
  std::string dirname;
  split_filename( name, dirname, basename );
  return basename;
  }


bool Menu::test_slash_filename( std::string & name ) throw()
  {
  tilde_expansion( name );
  return ( !name.size() ||
           ( valid_filename( name ) && !append_slash_if_dir( name ) ) );
  }


bool Menu::tilde_expansion( std::string & name ) throw()
  {
  if( name.size() >= 2 && !name.compare( 0, 2, "~/" ) )
    { name.replace( 0, 1, RC::home_directory() ); return true; }
  return false;
  }
