// -*- Mode: C++ -*-
//
// Copyright (C) 2013-2020 Red Hat, Inc.
//
// This file is part of the GNU Application Binary Interface Generic
// Analysis and Instrumentation Library (libabigail).  This library is
// free software; you can redistribute it and/or modify it under the
// terms of the GNU Lesser General Public License as published by the
// Free Software Foundation; either version 3, or (at your option) any
// later version.

// This library 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 Lesser Public License for more details.

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

#include <sys/types.h>
#include <sys/stat.h>// The two above is for 'stat'
#include <cstring> // for 'strcmp'
#include <cstdlib> // for 'system'
#include <iostream>
#include <fstream>
#include <list>
#include "abg-corpus.h"
#include "abg-reader.h"
#include "abg-writer.h"
#include "abg-tools-utils.h"

using std::cerr;
using std::cout;
using std::ostream;
using std::string;
using std::list;

struct options
{
  bool list_content;
  string extract_dest;
  string archive;
  list<string> in_files;
  string out_dir;

  options()
    : list_content(false)
  {
  }

};//end options

/// Display a message explaining the usage of the program on stdout.
static void
display_usage(const string &prog_name, ostream& out)
{
  out << "usage: " << prog_name << " [options] [archive-file-path]\n"
       << " where options are: \n"
       << "--help|-h				display this usage message\n"
       << "--list|l <archive>			list the archive content\n"
       << "--add|-a <files-to-add> <archive>	add files to an archive\n"
       << "--extract|x [dest-dir] <archive>	extract archive content\n"
  ;
}

/// Parse the command line arguments and populate an instance of \a
/// options as a result.
///
/// @param argc the number of words on the command line, including the
/// program name.
///
/// @param argv an array of words representing the command line.  This
/// is the thing that is parsed.
///
/// @param opts the options that are set as a result of parsing the
/// command line.  This output parameter is set iff the function
/// returs true.
///
/// @return true if the function could make sense of the command line
/// and parsed it correctly.  Otherwise, return false.  If this
/// function returns false, the caller would be well inspired to call
/// display_usage() to remind the user how to properly invoke this
/// program so that its command line makes sense.
static bool
parse_args(int argc, char* argv[], options& opts)
{
  if (argc < 2)
    return false;

  for (int i = 1; i < argc; ++i)
    {
      char* arg = argv[i];

      if (! strcmp(arg, "--help")
	  ||! strcmp(arg, "-h"))
	return false;

      if (arg[0] != '-')
	opts.archive = arg;
      else if (! strcmp(arg, "--list")
	       ||! strcmp(arg, "-l"))
	opts.list_content = true;
      else if (! strcmp(arg, "--add")
	       || ! strcmp(arg,"-a"))
	{
	  int arg_index0, arg_index1;
	  char *f = 0;
	  for (arg_index0 = i + 1; arg_index0 < argc; ++arg_index0)
	    {
	      // --add must be followed by at N words that don't start
	      // by '-' (N > 1).  The first N-1 words are the
	      // arguments of --add (the files to add to the archive)
	      // and the last one is the name of the archive to add
	      // the files to.

	      f = argv[arg_index0];
	      if (f[0] == '-')
		break;

	      arg_index1 = arg_index0 + 1;
	      if (arg_index1 >= argc
		  || argv[arg_index1][0] == '-')
		break;

	      opts.in_files.push_back(f);
	    }
	  if (opts.in_files.empty())
	    return false;
	}
      else if (! strcmp(arg, "--extract")
			|| ! strcmp(arg, "-x"))
	{
	  int arg_index = i + 1, arch_index = arg_index + 1;
	  if (arg_index < argc
	      && argv[arg_index][0] != '-'
	      && arch_index < argc
	      && argv[arch_index][0] != '-')
	    opts.extract_dest = argv[arg_index];
	  else if (arg_index < argc
		   && argv[arg_index][0] != '-')
	    // No destination directory argument was given for the
	    // --extract option, so consider it to be the current
	    // directory
	    opts.extract_dest = ".";
	  else
	    return false;
	}
    }

  return true;
}

using abigail::corpus;
using abigail::corpus_sptr;
using abigail::translation_unit;
using abigail::translation_unit_sptr;
using abigail::translation_units;
using abigail::xml_reader::read_corpus_from_file;
using abigail::xml_writer::write_corpus_to_archive;

/// List the content of a given archive.  The names of the files of
/// the archive are then displayed on stdout.
///
/// @param archive_path a path to the file containing the archive file
/// to list the content of.
///
/// @return true upon successful completion, false otherwise.
static bool
list_content(const string& archive_path)
{
  if (archive_path.empty())
    {
      cerr << "Empty archive path\n";
      return false;
    }

  corpus_sptr archive = read_corpus_from_file(archive_path);
  if (!archive)
    {
      cerr << "Could not read archive at '" << archive_path << "'\n";
      return false;
    }

  for (translation_units::const_iterator i =
	 archive->get_translation_units().begin();
       i != archive->get_translation_units().end();
       ++i)
    cout << (*i)->get_path() << "\n";

  return true;
}

/// Add a translation unit to an archive -- or create one for that
/// matter.
///
/// @param tu_paths a list of paths to add to the archive.
///
/// @param archive_path the path of the archive to either open or
/// create.  The specified in \tu_paths are then added to this
/// archive.  Note that this function creates the entire directory
/// tree needed up to \a archive_path, if needed.
///
/// @return true upon successful completion, false otherwise.
static bool
add_tus_to_archive(const list<string>& tu_paths,
		   const string& archive_path)
{
  translation_units tus;
  corpus corp(archive_path);

  bool added_some_tus = false;
  for (list<string>::const_iterator i = tu_paths.begin();
       i != tu_paths.end();
       ++i)
    {
      translation_unit_sptr tu =
	abigail::xml_reader::read_translation_unit_from_file(*i);
      if (!tu)
	{
	  cerr << "could not read binary instrumentation file '"
	       << *i
	       << "'\n";
	  continue;
	}
      corp.add(tu);
      added_some_tus = true;
    }

  if (added_some_tus)
    {
      if (!write_corpus_to_archive(corp))
	{
	  cerr << "could not write archive file '"
	       << corp.get_path()
	       << "'\n";
	  return false;
	}
    }

  return true;
}

/// Extract translation units from a given archive.
///
/// @param dest_path the path of the destination directory which the
/// elements of the archive are to be extracted under.
///
/// @param archive_path the path to the archive to extract.  The
/// archive must exist and be accessible with read privileges.
///
/// @return true upon successful completion, false otherwise.
static bool
extract_tus_from_archive(const string& dest_path,
			 const string& archive_path)
{
  if (dest_path.empty())
    {
      cerr << "empty file directory\n";
      return false;
    }

  corpus_sptr archive(new corpus(archive_path));

  if (read_corpus_from_file(archive) < 1)
    {
      cerr << "could not read archive at '"
	   << archive_path
	   << "'\n;";
      return false;
    }

  string cmd = "mkdir -p " + dest_path;
  if (system(cmd.c_str()))
    {
      cerr << "could not create file directory '"
	   << dest_path
	   << "'\n";
      return false;
    }

  for (translation_units::const_iterator i =
	 archive->get_translation_units().begin();
       i != archive->get_translation_units().end();
       ++i)
    {
      string dest = dest_path + "/" + (*i)->get_path();
      if (!abigail::tools_utils::ensure_parent_dir_created(dest))
	{
	  cerr << "could not create parent director for '" << dest << "'\n";
	  return false;
	}

      if (!abigail::xml_writer::write_translation_unit(**i, /*indent=*/0, dest))
	{
	  cerr << "could not write binary instrumentation file to '"
	       << dest
	       << "'\n";
	  return false;
	}
    }

  return true;
}

/// Parse the command line and perform the archive-related operations
/// asked by the user, if the command line makes sense; otherwise,
/// display a usage help message and bail out.
int
main(int argc, char* argv[])
{
  options opts;

  if (!parse_args(argc, argv, opts)
      || opts.archive.empty())
    {
      display_usage(argv[0], cout);
      return -1;
    }

  if (opts.list_content)
    return !list_content(opts.archive);
  else if (!opts.in_files.empty())
    return !add_tus_to_archive(opts.in_files, opts.archive);
  else if (!opts.extract_dest.empty())
    return !extract_tus_from_archive(opts.extract_dest, opts.archive);
  else
    {
      display_usage(argv[0], cout);
      return -1;
    }

  return 0;
}
