//-----------------------------------------------------------
// Copyright Christian Arnault LAL-Orsay CNRS
// arnault@lal.in2p3.fr
// See the complete license in cmt_license.txt "http://www.cecill.info". 
//-----------------------------------------------------------

#ifdef WIN32
#include <direct.h>
#define popen _popen
#define pclose _pclose
#endif

#include "cmt_awk.h"
#include "cmt_system.h"
#include "cmt_log.h"
#include "cmt_use.h"

class Parser
{
public:
  Parser (Awk* awk, const cmt_string pattern, const cmt_regexp* expression) :
    m_pattern (pattern), m_expression (expression), m_awk(awk)
  {
  }

  /** 
   *  this first level parsing function extracts individual lines
   *  from the text, taking care of both Unix and Windows EOL styles.
   *
   *   Then the second level parsing function parse_line is called.
   */
  Awk::condition parse (const cmt_string& text)
  {
    Awk::condition result = Awk::ok;

    cmt_string line;
    int pos;
    int max_pos;

    pos = 0;
    max_pos = text.size ();

    m_accumulator.erase (0);

    for (pos = 0; pos < max_pos;)
      {
	int eol = text.find (pos, '\n');
            
	if (eol == cmt_string::npos)
	  {
	    // Last line, since there is no eol at all
	    text.substr (pos, line);
	    pos = max_pos;
	  }
	else
	  {
	    int length = 1;

	    int cr = text.find (pos, "\r\n");

	    if (cr == (eol-1))
	      {
		eol = cr;
		length = 2;
	      }

	    if (eol == pos)
	      {
		// this is an empty line
		line = "";
		pos += length;
	      }
	    else
	      {
		// The eol was found beyond the current position
		// (ie. this is a non empty line)
		text.substr (pos, eol - pos, line);
		pos = eol + length;
	      }
	  }

	if (m_awk != 0) m_awk->inc_line_number ();

	result = parse_line (line);
	if (result != Awk::ok) break;
      }

    return (result);
  }

  /**
   *   This second level parsing function accumulates individual lines
   *   with real trailing back slashes.
   *    Eventually the possible text pattern or regular expression is
   *   checked and the Awk::filter function is called in case of
   *   succesful match onto the accumulated line.
   */
  Awk::condition parse_line (const cmt_string& line)
  {
    Awk::condition result = Awk::ok;
    int length;
    cmt_string temp_line = line;

    //
    // We scan the line for handling backslashes.
    //
    // Really terminating backslashes (ie those only followed by spaces/tabs
    // mean continued line
    //
    //

    bool finished = true;

    length = temp_line.size ();

    if (length == 0)
      {
	// An empty line following a backslash terminates the continuation.
	finished = true;
      }
    else
      {
	int back_slash = temp_line.find_last_of ('\\');
        
	if (back_slash != cmt_string::npos)
	  {
	    //
	    // This is the last backslash
	    // check if there are only space chars after it
	    //
            
	    bool at_end = true;
            
	    for (int i = (back_slash + 1); i < length; i++)
	      {
		char c = temp_line[i];
		if ((c != ' ') && (c != '\t'))
		  {
		    at_end = false;
		    break;
		  }
	      }
                
	    if (at_end)
	      {
		temp_line.erase (back_slash);
		finished = false;
	      }
	    else
	      {
		// This was not a trailing backslash.
		finished = true;
	      }
	  }
        
	m_accumulator += temp_line;
      }

    if (!finished)
      {
	// We still need to accumulate forthcoming lines
	// before parsing the resulting text.
	return (Awk::ok);
      }

    // now filter the complete accumulated line (if non empty)

    if (m_accumulator != "")
      {
	bool ok = false;
            
	if (m_expression != 0)
	  {
	    if (m_expression->match (m_accumulator))
	      {
		ok = true;
	      }
	  }
	else
	  {
	    if ((m_pattern == "") ||
		(m_accumulator.find (m_pattern) != cmt_string::npos))
	      {
		ok = true;
	      }
	  }
            
	if (ok && (m_awk != 0))
	  {
	    m_awk->filter (m_accumulator);
	    result = m_awk->get_last_condition ();
	  }

	m_accumulator.erase (0);
      }
        
    return (result);
  }

private:

  cmt_string m_accumulator;
  cmt_string m_pattern;
  const cmt_regexp* m_expression;
  Awk* m_awk;
};

//------------------------------------------------
Awk::Awk ()
{
  m_condition = ok;
}

//------------------------------------------------
Awk::~Awk ()
{
}

//------------------------------------------------
Awk::condition Awk::run (const cmt_string& text,
                         const cmt_string& pattern)
{
  m_line_number = 0;
  m_condition = ok;

  begin ();
  if (m_condition != ok) return (m_condition);

  if (CmtSystem::testenv ("CMTTESTAWK"))
    {
      Parser p (this, pattern, 0);

      m_condition = p.parse (text);
      if (m_condition != ok) return (m_condition);
    }
  else
    {
      cmt_string line;
      int pos = 0;
      int max_pos;

      max_pos = text.size ();

      for (pos = 0; pos < max_pos;)
        {
          int cr = text.find (pos, "\r\n");
          int nl = text.find (pos, '\n');
          
	  // Get the first end-of-line (either lf or cr-lf)

	  //--------------------
	  //
	  //     cr    1    0
	  //   nl
	  //
	  //    1      a    b
	  //
	  //    0      c    d
	  //
	  //--------------------
          
          int first = nl;
          
          if (cr != cmt_string::npos)
            {
	      // cases a or c

              if (nl == cmt_string::npos)
                {
		  // case a
                  first = cr;
                }
              else
                {
		  // case c
                  first = (nl < cr) ? nl : cr;
                }
            }
          
          if (first == cmt_string::npos)
            {
	      // This is likely the last line since there is no end-of-line
              text.substr (pos, line);
              pos = max_pos;
            }
          else if (first > pos)
            {
	      // The eol was found beyond the current position
	      // (ie. this is a non empty line)
              text.substr (pos, first - pos, line);
              pos = first + 1;
            }
          else
            {
	      // an empty line
              line = "";
              pos++;
            }
          
          m_line_number++;
          
          if (line != "")
            {
              if ((pattern == "") ||
                  (line.find (pattern) != cmt_string::npos))
                {
                  filter (line);
                  if (m_condition != ok) return (m_condition);
                }
            }
        }
    }

  end ();

  return (m_condition);
}

//------------------------------------------------
Awk::condition Awk::run (const cmt_string& text,
                         const cmt_regexp& expression)
{
  m_line_number = 0;
  m_condition = ok;

  begin ();
  if (m_condition != ok) return (m_condition);

  Parser p (this, "", &expression);

  m_condition = p.parse (text);
  if (m_condition != ok) return (m_condition);

  /*
    if (CmtSystem::testenv ("CMTTESTAWK"))
    {
    }
    else
    {
    cmt_string line;
    int pos = 0;
    int max_pos;

    max_pos = text.size ();

    for (pos = 0; pos < max_pos;)
    {
    int cr = text.find (pos, "\r\n");
    int nl = text.find (pos, '\n');
          
    // Get the first end-of-line (either lf or cr-lf)
          
    int first = nl;
          
    if (cr != cmt_string::npos)
    {
    if (nl == cmt_string::npos)
    {
    first = cr;
    }
    else
    {
    first = (nl < cr) ? nl : cr;
    }
    }
          
    if (first == cmt_string::npos)
    {
    // This is likely the last line since there is no end-of-line
    text.substr (pos, line);
    pos = max_pos;
    }
    else if (first > pos)
    {
    // The eol was found beyond the current position
    // (ie. this is a non empty line)
    text.substr (pos, first - pos, line);
    pos = first + 1;
    }
    else
    {
    // an empty line
    line = "";
    pos++;
    }
          
    m_line_number++;
          
    if (line != "")
    {
    if (expression.match (line))
    {
    filter (line);
    if (m_condition != ok) return (m_condition);
    }
    }
    }
    }
  */

  end ();

  return (m_condition);
}

//------------------------------------------------
void Awk::stop ()
{
  m_condition = stopped;
}

//------------------------------------------------
void Awk::abort ()
{
  m_condition = failed;
}

//------------------------------------------------
void Awk::allow_continuation ()
{
  m_continuation_allowed = true;
}

//------------------------------------------------
Awk::condition Awk::get_last_condition () const
{
  return (m_condition);
}

//------------------------------------------------
void Awk::begin ()
{
}

//------------------------------------------------
void Awk::filter (const cmt_string& /*line*/)
{
}

//------------------------------------------------
void Awk::end ()
{
}

//------------------------------------------------
void Awk::inc_line_number ()
{
  m_line_number++;
}

//------------------------------------------------
Awk::condition FAwk::run (const cmt_string& file_name,
                          const cmt_string& pattern)
{
  if (!CmtSystem::test_file (file_name)) return (failed);

  CmtSystem::basename (file_name, m_file_name);
  CmtSystem::dirname (file_name, m_dir_name);

  cmt_string text;

  text.read (file_name);

  return (Awk::run (text, pattern));
}

//------------------------------------------------
Awk::condition FAwk::run (const cmt_string& file_name,
                          const cmt_regexp& expression)
{
  if (!CmtSystem::test_file (file_name)) return (failed);

  CmtSystem::basename (file_name, m_file_name);
  CmtSystem::dirname (file_name, m_dir_name);

  cmt_string text;

  text.read (file_name);

  return (Awk::run (text, expression));
}

//------------------------------------------------
Awk::condition PAwk::run (const cmt_string& command, 
                          const cmt_string& pattern)
{
  cmt_string line;

  m_line_number = 0;
  m_condition = ok;

  begin ();
  if (m_condition != ok) return (m_condition);

  FILE* f = popen (command.c_str (), "r"); 
  
  if (f == 0) return (failed);

  char buffer[8192]; 
  char* ptr;

  while ((ptr = fgets (buffer, sizeof (buffer), f)) != NULL) 
    {
      line = ptr;

      if (line.find ("\n") == cmt_string::npos)
        {
	  CmtMessage::warning ("Line too long and truncated in PAwk::run for command " + command);
	  //          cerr << "#CMT> Warning: Line too long and truncated in PAwk::run for command " << command << endl;
        }

      line.replace ("\n", "");

      m_line_number++;

      if (line != "")
        {
          if ((pattern == "") ||
              (line.find (pattern) != cmt_string::npos))
            {
              filter (line);
              if (m_condition != ok) return (m_condition);
            }
        }
    }

  pclose (f);

  end ();

  return (m_condition);
}

//------------------------------------------------
Awk::condition PAwk::run (const cmt_string& command, 
                          const cmt_regexp& expression)
{
  cmt_string line;

  m_line_number = 0;
  m_condition = ok;

  begin ();
  if (m_condition != ok) return (m_condition);

  FILE* f = popen (command.c_str (), "r"); 
  
  if (f == 0) return (failed);

  char buffer[256]; 
  char* ptr;

  while ((ptr = fgets (buffer, sizeof (buffer), f)) != NULL) 
    {
      line = ptr;

      line.replace ("\n", "");

      m_line_number++;

      if (line != "")
        {
          if (expression.match (line))
            {
              filter (line);
              if (m_condition != ok) return (m_condition);
            }
        }
    }

  pclose (f);

  end ();

  return (m_condition);
}

//----------------------------------------------------------
PathScanner::PathScanner ()
{
  _running = false;
  _level = 0;
}

//----------------------------------------------------------
bool PathScanner::scan_path (const cmt_string& path, actor& a)
{
  if (_running) return (false);

  _level = 0;
  _running = true;

  cmt_string compressed_path = path;
  CmtSystem::compress_path (compressed_path);
  CmtSystem::realpath      (compressed_path, compressed_path);  
  scan_path                (compressed_path, 0, a);

  _running = false;
  _level = 0;

  return (true);
}

//----------------------------------------------------------
void PathScanner::scan_path (const cmt_string& path, int level, actor& a)
{
  if (level > 10)
    {
      return;
    }

  //
  // Only do something if it is a directory.
  //
  if (!CmtSystem::test_directory (path)) return;
  

  CmtSystem::cmt_string_vector list;
  CmtSystem::cmt_string_vector entrylist;

  CmtSystem::scan_dir (path, list);

  if (list.size () == 0) return;

  _level++;

  // Will be set if at least one directory is a version directory
  bool has_package = false;

  cmt_string name;
  cmt_string version;
  cmt_string where;

  int i;

  for (i = 0; i < list.size (); i++)
    {
      const cmt_string& here = list[i];

      if (!CmtSystem::test_directory (here)) continue;

      name    = "";
      version = "";

      cmt_string entry;
      CmtSystem::basename (here, entry);
      CmtSystem::dirname (path, where);

      if ((level == 0) && (entry == "InstallArea")) continue;
      if (entry == "CVS") continue;

      cmt_string req;

      req = here;
      req += CmtSystem::file_separator ();
      req += "mgr";
      req += CmtSystem::file_separator ();
      req += "requirements";
      
      if (CmtSystem::test_file (req))
	{
	  // We have found <path>/mgr/requirements
	  // this is an old directory convention. 
	  // The version directory is the directory above

	  version = entry;
	  CmtSystem::basename (path, name);

	  a.run (name, version, where);
	  has_package = true;

	  // We don't keep on looking in this directory since the versioned structure
	  // does not expect subpackages there
	  continue;
	}

      req = here;
      req += CmtSystem::file_separator ();
      req += "cmt";
      req += CmtSystem::file_separator ();
      req += "requirements";

      if (CmtSystem::test_file (req))
	{
	  // We have found <path>/cmt/requirements
	  // Question now is to detect the directory structure:
	  //
	  // if cmt/version.cmt exists it's a non-version-directory structure
	  // else
	  //   if there is a package statement in the requirements file we find it upward
	  //   else
	  //     if up is a version directory
	  //     else
	  //

	  cmt_string vreq;
	  vreq = here;
	  vreq += CmtSystem::file_separator ();
	  vreq += "cmt";
	  //	  vreq += CmtSystem::file_separator ();
	  //	  vreq += "version.cmt";

	  //	  if (CmtSystem::test_file (vreq))
	  if (CmtSystem::test_file (vreq
				    + CmtSystem::file_separator ()
				    + Package::get_version_file_name ())
	      )
	    {
	      Package::get_version (version, vreq);
	      
	      a.run (entry, version, path, true);
	      has_package = true;

	      // We scan further down
	      scan_path (here, 1, a);
	      continue;
	    }

	  cmt_string p;
	  
	  p.read (req);
	  int pos;
	  pos = p.find ("package");
	  if (pos != cmt_string::npos)
	    {
	      p.erase (0, pos+8);
	      pos = p.find ('\n');
	      if (pos != cmt_string::npos) p.erase (pos);
	      pos = p.find ('\r');
	      if (pos != cmt_string::npos) p.erase (pos);
	      p.replace_all (" ", "");
	      p.replace_all ("\t", "");
	      if (p != "") name = p;
	    }
	  
	  if (name != "")
	    {
	      // The package name was specified in the requirements file
	      
	      if (entry == name)
		{
		  // The structure is without the version directory.
		  
		  a.run (name, "v1", path);
		  has_package = true;
		  
		  // We scan further down
		  scan_path (here, 1, a);
		  continue;
		}
	      
	      version = entry;
	      CmtSystem::basename (path, entry);
	      
	      if (entry == name)
		{
		  // The structure is with the version directory.
		  
		  a.run (name, version, where);
		  has_package = true;
		  
		  continue;
		}
	      
	      // No directory structure matches the package name
	      // Is it a typo in the requirements file ?
	      // probably we should display it and quit...
	    }
	  else
	    {
	      version = entry;
	      CmtSystem::basename (path, entry);
	    }
	  
	  // The package name is not specified in the requirements file
	  // or did not match the directory structure
	  // We'll have to guess it from the structure
	  
	  if (CmtSystem::is_version_directory (version))
	    {
	      a.run (entry, version, where);
	      has_package = true;
	      
	      continue;
	    }

	  // Here we suppose that the directory up is the package name
	  // although no specification of the version has been found
	  // So we decide that
	  // - it's a non-versioned structure
	  // - the version is set by default to v1
	  name = version;
	  
	  where += CmtSystem::file_separator ();
	  where += entry;
	  
	  a.run (name, "v1", where);
	  has_package = true;
	  
	  // We scan further down
	  scan_path (here, 1, a);
	  continue;
	}

      //
      // Here this is a non-package directory. Let's search further down

      scan_path (here, level + 1, a);
    }

  if (has_package)
    {
      //
      // At least one version was found here. Thus we want to scan further down.
      //

      for (i = 0; i < entrylist.size (); i++)
	{
	  const cmt_string& e = entrylist[i];

	  cmt_string p = path;
	  p += CmtSystem::file_separator ();
	  p += e;

	  scan_path (p, 1, a);
	}
    }

  _level--;
}


//----------------------------------------------------------
bool PathScanner::scan_package (const cmt_string& path,
                                const cmt_string& package)
{
  //
  // Only do something if it is a directory.
  //

  if (!CmtSystem::test_directory (path)) return (false);

  cmt_string pattern = path;
  pattern += CmtSystem::file_separator ();
  pattern += package;

  if (!CmtSystem::test_directory (pattern)) return (false);

  CmtSystem::cmt_string_vector list;

  CmtSystem::scan_dir (pattern, list);

  if (list.size () == 0) 
    {
      return (false);
    }

  bool result = false;

  int i;
  for (i = 0; i < list.size (); i++)
    {
      const cmt_string& name = list[i];

      cmt_string version;
      CmtSystem::basename (name, version);

      if (version == "cmt")
        {
          cmt_string req;

          req = name;
          req += CmtSystem::file_separator ();
          req += "requirements";

          if (CmtSystem::test_file (req))
            {
	      cmt_string version;
	      cmt_string v;
	      if (Package::get_version (v, name))
		{
		  version = v;
		}
	      else
		{
		  version = "v*";
		}
	      
              cout << package << " " << version << " " << path << endl;

              result = true;
            }
        }
      else if (CmtSystem::is_version_directory (version))
        {
          cmt_string req;

          req = name;
          req += CmtSystem::file_separator ();
          req += "cmt";
          req += CmtSystem::file_separator ();
          req += "requirements";

          if (CmtSystem::test_file (req))
            {
              cout << package << " " << version << " " << path << endl;

              result = true;
            }
          else
            {
              req = name;
              req += CmtSystem::file_separator ();
              req += "mgr";
              req += CmtSystem::file_separator ();
              req += "requirements";

              if (CmtSystem::test_file (req))
                {
                  cout << package << " " << version << " " << path << endl;

                  result = true;
                }
              else
                {
                }
            }
        }
      else
        {
        }
    }

  return (result);
}

