// Copyright (c) 1996 James Clark
// See the file COPYING for copying permission.

// Need option registration method that allows derived class to change
// option names.

#ifdef __GNUG__
#pragma implementation
#endif

#include "splib.h"
#include "CmdLineApp.h"
#include "CmdLineAppMessages.h"
#include "MessageArg.h"
#include "ErrnoMessageArg.h"
#include "Options.h"
#include "version.h"
#include "xnew.h"
#include "macros.h"
#include "sptchar.h"
#include "MessageTable.h"
#include "CodingSystemKit.h"

#include "ConsoleOutput.h"

#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#ifdef SP_HAVE_LOCALE
#include <locale.h>
#endif
#ifdef SP_HAVE_SETMODE
#include <fcntl.h>
#include <io.h>
#endif

#include <sys/types.h>
#ifdef SP_INCLUDE_UNISTD_H
#include <unistd.h>
#endif
#ifdef SP_INCLUDE_IO_H
#include <io.h>
#endif

#ifdef _MSC_VER
#include <crtdbg.h>
#endif

#ifndef SP_DEFAULT_ENCODING
#ifdef WIN32
#define SP_DEFAULT_ENCODING SP_T("WINDOWS")
#else
#define SP_DEFAULT_ENCODING  SP_T("IS8859-1")
#endif
#endif /* not SP_DEFAULT_ENCODING */

#ifdef SP_NAMESPACE
namespace SP_NAMESPACE {
#endif

static const SP_TCHAR *progName = 0;

static const SP_TCHAR versionString[] = SP_VERSION;

static FileOutputByteStream standardOutput(1, 0);
static FileOutputByteStream standardError(2, 0);

CmdLineApp::CmdLineApp(const char *requiredInternalCode)
: errorFile_(0),
  outputCodingSystem_(0),
  // Colon at beginning is Posix.2ism that says to return : rather than ? for
  // missing option argument.
  optstr_(SP_T(":"), 1),
  MessageReporter(0),
  internalCharsetIsDocCharset_(1),
  codingSystem_(0)
{
  initCodingSystem(requiredInternalCode);
  setMessageStream(makeStdErr());
  registerOption('b', internalCharsetIsDocCharset_ ? SP_T("bctf") : SP_T("encoding"));
  registerOption('f', SP_T("error_file"));
  registerOption('v');
}

void CmdLineApp::resetCodingSystemKit()
{
  codingSystemKit_ = codingSystemKit_->copy();
}

void CmdLineApp::registerOption(AppChar c, const AppChar *argName)
{
  optstr_ += c;
  if (argName) {
    optstr_ += SP_T(':');
    optArgNames_.push_back(argName);
  }
}

StringC CmdLineApp::usageString()
{
  String<AppChar> result;
  if (progName)
    result.assign(progName, tcslen(progName));
  PackedBoolean hadOption[128];
  for (int i = 0; i < 128; i++)
    hadOption[i] = 0;
  Boolean hadNoArgOption = 0;
  for (size_t i = 1; i < optstr_.size(); i++) {
    if (optstr_[i] == 0)
      break;
    if (i + 1 < optstr_.size() && optstr_[i + 1] == ':')
      i++;
    else if (!hadOption[optstr_[i]]) {
      hadOption[optstr_[i]] = 1;
      if (!hadNoArgOption) {
	hadNoArgOption = 1;
	result.append(SP_T(" [-"), 3);
      }
      result += optstr_[i];
    }
  }
  if (hadNoArgOption)
    result += SP_T(']');
  size_t j = 0;
  for (size_t i = 1; i < optstr_.size(); i++) {
    if (i + 1 < optstr_.size() && optstr_[i + 1] == ':') {
      if (!hadOption[optstr_[i]]) {
	hadOption[optstr_[i]] = 1;
	result += SP_T(' ');
	result += SP_T('[');
	result += SP_T('-');
	result += optstr_[i];
	result += SP_T(' ');
	result.append(optArgNames_[j], tcslen(optArgNames_[j]));
	result += SP_T(']');
      }
      i++;
      j++;
    }
  }
  result.append(SP_T(" sysid..."), tcslen(SP_T(" sysid...")));
  result += 0;
  return convertInput(result.data());
}

static
void ewrite(const char *s)
{
  int n = (int)strlen(s);
  while (n > 0) {
    int nw = write(2, s, n);
    if (nw < 0)
      break;
    n -= nw;
    s += nw;
  }
}

static
#ifdef SP_FANCY_NEW_HANDLER
int outOfMemory(size_t)
#else
void outOfMemory()
#endif
{
  ewrite("SP library: out of memory\n");
  exit(1);
#ifdef SP_FANCY_NEW_HANDLER
  return 0;
#endif  
}

int CmdLineApp::init(int, AppChar **argv)
{
#ifndef SP_ANSI_LIB
  set_new_handler(outOfMemory);
#endif
#ifdef SP_HAVE_LOCALE
  setlocale(LC_ALL, "");
#endif
#ifdef SP_HAVE_SETMODE
  _setmode(1, _O_BINARY);
  _setmode(2, _O_BINARY);
#endif
  progName = argv[0];
  if (progName)
    setProgramName(convertInput(progName));
  return 0;
}

int CmdLineApp::run(int argc, AppChar **argv)
{
#ifdef _MSC_VER
  _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);
#endif
#ifdef SP_ANSI_LIB
  try {
#endif
  int ret = init(argc, argv);
  if (ret)
    return ret;
  int firstArg;
  ret = processOptions(argc, argv, firstArg);
  if (ret)
    return ret;
  ret = processArguments(argc - firstArg, argv + firstArg);
  progName = 0;
  return ret;
#ifdef SP_ANSI_LIB
  }
catch (
#ifndef SP_NO_STD_NAMESPACE
       std::
#endif
	    bad_alloc) {
#ifdef SP_FANCY_NEW_HANDLER
    outOfMemory(0);
#else
    outOfMemory();
#endif
  }
  return 1;
#endif /* SP_ANSI_LIB */
}    

int CmdLineApp::processOptions(int argc, AppChar **argv, int &nextArg)
{
  AppChar ostr[2];
  optstr_ += SP_T('\0');
  Options<AppChar> options(argc, argv, optstr_.data());
  AppChar opt;
  while (options.get(opt)) {
    switch (opt) {
    case ':':
      ostr[0] = options.opt();
      ostr[1] = SP_T('\0');
      message(CmdLineAppMessages::missingOptionArgError,
	      StringMessageArg(convertInput(ostr)));
      message(CmdLineAppMessages::usage,
	      StringMessageArg(usageString()));
      return 1;
    case '?':
      ostr[0] = options.opt();
      ostr[1] = SP_T('\0');
      message(CmdLineAppMessages::invalidOptionError,
	      StringMessageArg(convertInput(ostr)));
      message(CmdLineAppMessages::usage,
	      StringMessageArg(usageString()));
      return 1;
    default:
      processOption(opt, options.arg());
      break;
    }
  }
  nextArg = options.ind();
  if (errorFile_) {
    static FileOutputByteStream file;
    if (!file.open(errorFile_)) {
      message(CmdLineAppMessages::openFileError,
	      StringMessageArg(convertInput(errorFile_)),
	      ErrnoMessageArg(errno));
      return 1;
    }
    setMessageStream(new EncodeOutputCharStream(&file, codingSystem()));
  }
  if (!outputCodingSystem_)
    outputCodingSystem_ = codingSystem();
  return 0;
}

void CmdLineApp::processOption(AppChar opt, const AppChar *arg)
{
  switch (opt) {
  case 'b':
    outputCodingSystem_ = lookupCodingSystem(arg);
    if (!outputCodingSystem_)
      message(internalCharsetIsDocCharset_
	      ? CmdLineAppMessages::unknownBctf
	      : CmdLineAppMessages::unknownEncoding,
	      StringMessageArg(convertInput(arg)));
    break;
  case 'f':
    errorFile_ = arg;
    break;
  case 'v':
    // print the version number
    message(CmdLineAppMessages::versionInfo,
	    StringMessageArg(convertInput(versionString)));
    break;
  default:
    CANNOT_HAPPEN();
  }
}

Boolean CmdLineApp::getMessageText(const MessageFragment &frag,
				   StringC &text)
{
  String<SP_TCHAR> str;
  if (!MessageTable::instance()->getText(frag, str))
    return 0;
#ifdef SP_WIDE_SYSTEM
  text.assign((const Char *)str.data(), str.size());
#else
  str += 0;
  text = codingSystem()->convertIn(str.data());
#endif
  return 1;
}

Boolean CmdLineApp::stringMatches(const SP_TCHAR *s, const char *key)
{
  for (; *key != '\0'; s++, key++) {
    if (*s != tolower(*key) && *s != toupper(*key))
      return 0;
  }
  return *s == '\0';
}

void CmdLineApp::initCodingSystem(const char *requiredInternalCode)
{
  const char *name = requiredInternalCode;
#ifdef SP_MULTI_BYTE
  char buf[256];
  if (!name) {
    const SP_TCHAR *internalCode = tgetenv(SP_T("SP_SYSTEM_CHARSET"));
    if (internalCode) {
      buf[255] = '\0';
      for (size_t i = 0; i < 255; i++) {
	buf[i] = internalCode[i];
	if (buf[i] == '\0')
	  break;
      }
      name = buf;
    }
  }
  if (requiredInternalCode)
    internalCharsetIsDocCharset_ = 0;
  else {
    const SP_TCHAR *useInternal = tgetenv(SP_T("SP_CHARSET_FIXED"));
    if (useInternal
        && (stringMatches(useInternal, "YES")
	    || stringMatches(useInternal, "1")))
      internalCharsetIsDocCharset_ = 0;
  }
#endif /* SP_MULTI_BYTE */
  codingSystemKit_ = CodingSystemKit::make(name);
  const SP_TCHAR *codingName = tgetenv(internalCharsetIsDocCharset_
				       ? SP_T("SP_BCTF")
				       : SP_T("SP_ENCODING"));
  if (codingName)
    codingSystem_ = lookupCodingSystem(codingName);
#ifdef SP_MULTI_BYTE
  if (!codingSystem_ && !internalCharsetIsDocCharset_)
    codingSystem_ = lookupCodingSystem(SP_DEFAULT_ENCODING);
#endif
  if (!codingSystem_
#ifndef SP_WIDE_SYSTEM
      || codingSystem_->fixedBytesPerChar() > 1
#endif
    )
    codingSystem_ = codingSystemKit_->identityCodingSystem();
}

const CodingSystem *
CmdLineApp::lookupCodingSystem(const AppChar *codingName)
{
#define MAX_CS_NAME 50
  if (tcslen(codingName) < MAX_CS_NAME) {
    char buf[MAX_CS_NAME];
    int i;
    for (i = 0; codingName[i] != SP_T('\0'); i++) {
      SP_TUCHAR c = codingName[i];
#ifdef SP_WIDE_SYSTEM
      if (c > (unsigned char)-1)
	return 0;
#endif
      buf[i] = char(c);
    }
    buf[i] = '\0';
    return codingSystemKit_->makeCodingSystem(buf, internalCharsetIsDocCharset_);
  }
  return 0;
}

StringC CmdLineApp::convertInput(const SP_TCHAR *s)
{
#ifdef SP_WIDE_SYSTEM
  StringC str(s, wcslen(s));
#else
  StringC str(codingSystem()->convertIn(s));
#endif
  for (size_t i = 0; i < str.size(); i++)
    if (str[i] == '\n')
      str[i] = '\r';
  return str;
}

OutputCharStream *CmdLineApp::makeStdErr()
{
  OutputCharStream *os = ConsoleOutput::makeOutputCharStream(2);
  if (os)
    return os;
  return new EncodeOutputCharStream(&standardError, codingSystem());
}

OutputCharStream *CmdLineApp::makeStdOut()
{
  OutputCharStream *os = ConsoleOutput::makeOutputCharStream(1);
  if (os)
    return os;
  return new EncodeOutputCharStream(&standardOutput, outputCodingSystem_);
}

const MessageType2 &CmdLineApp::openFileErrorMessage()
{
  return CmdLineAppMessages::openFileError;
}

const MessageType2 &CmdLineApp::closeFileErrorMessage()
{
  return CmdLineAppMessages::closeFileError;
}

#ifdef SP_NAMESPACE
}
#endif
