//-----------------------------------------------------------
// 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"

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 ();

              //cout << "parse> line=[" << line << "]" << endl;

            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;
          }

          //cout << "parse_line1> accumulator=[" << m_accumulator << "]" << endl;
          //cout << "parse_line1> finished=[" << finished << "]" << endl;

        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))
              {
                  //cout << "parse_line> accumulator=[" << m_accumulator << "]" << endl;

                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*/)
{
    //cout << "awk> " << line << endl;
}

//------------------------------------------------
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)
        {
          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)
    {
      //cout << "#PathScanner::scan_path> too deep search path=" << path << endl;
      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);

      //cout << "## here=" << here << " entry=" << entry << " where=" << where << endl;

      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);

	  //cout << "#1" << endl;

	  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))
	    {
	      version.read (vreq);
	      int pos;
	      pos = version.find ('\n');
	      if (pos != cmt_string::npos) version.erase (pos);
	      pos = version.find ('\r');
	      if (pos != cmt_string::npos) version.erase (pos);

	      //cout << "#2" << endl;

	      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.
		  
		  //cout << "#3" << endl;
		  
		  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.
		  
		  //cout << "#4" << endl;
		  
		  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))
	    {
	      //cout << "#5" << endl;
	      
	      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;
	  
	  //cout << "#6" << endl;
	  
	  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
      //cout << "#7" << endl;

      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;


	  /*
	  for (int j = 1; j < _level; j++) cout << "  ";
	  cout << "Restarting scan_path on p=" << p << endl;
            

	  cout << "#PathScanner::scan_path> Restarting scan_path on p=" << p << endl;
	  */

	  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))
            {
              //cout << " -> no version" << endl;

	      cmt_string req;
	      
	      req = name;
	      req += CmtSystem::file_separator ();
	      req += "version.cmt";

	      cmt_string version;
	      if (CmtSystem::test_file (req))
		{
		  version.read (req);
		  int pos;
		  pos = version.find ('\n');
		  if (pos != cmt_string::npos) version.erase (pos);
		  pos = version.find ('\r');
		  if (pos != cmt_string::npos) version.erase (pos);
		}
	      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 << " -> cmt" << endl;

              cout << package << " " << version << " " << path << endl;

              result = true;
            }
          else
            {
              //cout << " -> no cmt" << endl;

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

              if (CmtSystem::test_file (req))
                {
                  //cout << " -> mgr" << endl;

                  cout << package << " " << version << " " << path << endl;

                  result = true;
                }
              else
                {
                  //cout << " -> no mgr" << endl;
                }
            }
        }
      else
        {
          //cout << " -> stop" << endl;
        }
    }

  return (result);
}

