/***************************************************************************
 *   Copyright (C) 2006 Nicolas Hadacek <hadacek@kde.org>                  *
 *                                                                         *
 *   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 2 of the License, or     *
 *   (at your option) any later version.                                   *
 ***************************************************************************/
#include "cli_interactive.h"

#if defined(HAVE_READLINE)
#  include <readline/readline.h>
#  include <readline/history.h>
#else
#  include <stdio.h>
#endif
#include <signal.h>
#include <tqtimer.h>
#include <stdlib.h>

#include "progs/base/generic_prog.h"
#include "cmdline.h"
#include "cli_prog_manager.h"
#include "common/cli/cli_log.h"
#include "devices/base/device_group.h"
#include "devices/pic/base/pic_register.h"
#include "devices/pic/prog/pic_prog.h"
#include "coff/base/text_coff.h"
#include "devices/pic/prog/pic_debug.h"

//-----------------------------------------------------------------------------
const CLI::CommandData CLI::INTERACTIVE_COMMAND_DATA[] = {
  { "property-list",   NoCommandProperty, I18N_NOOP("Display the list of available properties.") },
  { "register-list",   NoCommandProperty, I18N_NOOP("Display the list of registers.") },
  { "variable-list",   NoCommandProperty, I18N_NOOP("Display the list of variables.") },
  { "range-list",      NoCommandProperty, I18N_NOOP("Display the list of memory ranges.") },
  { "help",            NoCommandProperty, I18N_NOOP("Display help.") },
  { "quit",            NoCommandProperty, I18N_NOOP("Quit.") },
  { "set",             NoCommandProperty, I18N_NOOP("Set property value: \"set <property> <value>\" or \"<property> <value>\".") },
  { "unset",           NoCommandProperty, I18N_NOOP("Unset property value: \"unset <property>\".") },
  { "get",             NoCommandProperty, I18N_NOOP("Get property value: \"get <property>\" or \"<property>\".") },
  { "disconnect",      NeedProgrammer, I18N_NOOP("Disconnect programmer.") },
  { "start",           NeedDevice | NeedProgrammer, I18N_NOOP("Start or restart debugging session.") },
  { "step",            NeedDevice | NeedProgrammer, I18N_NOOP("Step one instruction.") },
  { "x",               NeedDevice | NeedProgrammer, I18N_NOOP("Read or set a register or variable: \"x PORTB\" or \"x W 0x1A\".") },
  { "break",           NeedDevice | NeedProgrammer, I18N_NOOP("Set a breakpoint \"break e 0x04\" or list all breakpoints \"break\".") },
  { "clear",           NeedDevice | NeedProgrammer, I18N_NOOP("Clear a breakpoint \"clear 0\", \"clear e 0x04\", or clear all breakpoints \"clear all\".") },
  { "raw-com",         NeedDevice | NeedProgrammer, I18N_NOOP("Write and read raw commands to port from given file.") },
  { 0, NoCommandProperty, 0 }
};

const CLI::PropertyData CLI::PROPERTY_DATA[] = {
  { "programmer",   "programmer <name>",  "p", I18N_NOOP("Programmer to use."), "programmer-list", I18N_NOOP("Return the list of supported programmers.") },
  { "hardware",     "hardware <name>",    "h", I18N_NOOP("Programmer hardware configuration to use (for direct programmer)."), "hardware-list", I18N_NOOP("Return the list of supported programmer hardware configurations.") },
  { "device",       "device <name>",      "d", I18N_NOOP("Target device."), "device-list", I18N_NOOP("Return the list of supported devices.") },
  { "format",       "format <name>",      "f", I18N_NOOP("Hex output file format."), "format-list", I18N_NOOP("Return the list of supported hex file formats.") },
  { "port",         "port <name>",        "t", I18N_NOOP("Programmer port (\"usb\" or device such as \"/dev/ttyS0\")"), "port-list", I18N_NOOP("Return the list of detected ports.") },
  { "firmware-dir", "firmware-dir <dir>", 0,   I18N_NOOP("Firmware directory."), 0, 0 },
  { "target-self-powered", "target-self-powered <true|false>", 0, I18N_NOOP("Set if target device is self-powered."), 0, 0 },
  { "hex", 0, 0, I18N_NOOP("Hex file to be used for programming."), 0, 0 },
  { "coff", 0, 0, I18N_NOOP("COFF file to be used for debugging."), 0, 0 },
  { "processor",    0, 0, I18N_NOOP("Same as \"device\"."), 0, 0 },
  { 0, 0, 0, 0, 0, 0 }
};

//-----------------------------------------------------------------------------
void CLI::Interactive::signalHandler(int n)
{
  if ( n!=SIGINT ) return;
  fprintf(stdout, "<CTRL C> Break\n");
  fflush(stdout);
  Programmer::Base *prog = Programmer::manager->programmer();
  if ( prog && prog->state()==Programmer::Running ) Programmer::manager->halt();
  _interactive->redisplayPrompt();
}

CLI::Interactive::Interactive(TQObject *parent)
  : TQObject(parent, "interactive")
{
  setView(_view);
  ::signal(SIGINT, signalHandler);
#if defined(HAVE_READLINE)
  using_history();
#else
  _stdin.open(IO_ReadOnly, stdin);
#endif
  TQTimer::singleShot(0, this, TQ_SLOT(displayPrompt()));
}

void CLI::Interactive::redisplayPrompt()
{
#if defined(HAVE_READLINE)
  rl_forced_update_display();
#else
  fprintf(stdout, "> ");
  fflush(stdout);
#endif
}

void CLI::Interactive::displayPrompt()
{
#if defined(HAVE_READLINE)
  char *line = readline("> ");
  _input = TQString(line);
  if ( !_input.isEmpty() ) add_history(line);
  free(line);
#else
  fprintf(stdout, "> ");
  fflush(stdout);
  char buffer[1000];
  _stdin.readLine(buffer, 1000);
  _input = TQString(buffer);
#endif
  lineRead();
}

CLI::ExitCode CLI::Interactive::processLine(const TQString &s)
{
  TQStringList words = TQStringList::split(" ", s);
  if ( words.count()==0 ) return ARG_ERROR;
  if ( words[0]=="command-list" || words[0]=="property-list" || words[0]=="range-list"
       || isPropertyList(words[0]) ) return static_cast<Main *>(_main)->list(words[0]);
  if ( words[0]=="register-list" ) return registerList();
  if ( words[0]=="variable-list" ) return variableList();
  if ( isProperty(words[0]) ) {
    if ( words.count()==1 ) words.prepend("get");
    else if ( words.count()==2 ) words.prepend("set");
    else return errorExit(i18n("This command takes no or one argument"), ARG_ERROR);
  }
  if ( findCommand(words[0])!=OK ) return ARG_ERROR;
  const CommandData *data = findCommandData(words[0]);
  if ( words[0]=="quit" ) return EXITING;
  if ( words[0]=="set" ) {
    if ( words.count()==2 ) return static_cast<Main *>(_main)->executeSetCommand(words[1], TQString());
    if ( words.count()!=3 ) return errorExit(i18n("This command takes two arguments."), ARG_ERROR);
    return static_cast<Main *>(_main)->executeSetCommand(words[1], words[2]);
  }
  if ( words[0]=="unset" ) {
    if ( words.count()!=2 ) return errorExit(i18n("This command takes one argument."), ARG_ERROR);
    return static_cast<Main *>(_main)->executeSetCommand(words[1], TQString());
  }
  if ( words[0]=="get" ) {
    if ( words.count()!=2 ) return errorExit(i18n("This command takes one argument."), ARG_ERROR);
    TQString s = static_cast<Main *>(_main)->executeGetCommand(words[1]);
    if ( !s.isEmpty() ) log(Log::LineType::Normal, words[1] + ": " + s);
    return OK;
  }
  if ( words[0]=="help" ) return static_cast<Main *>(_main)->list("command-list");
  if ( words[0]=="x" ) {
    if ( words.count()!=2 && words.count()!=3 ) return errorExit(i18n("This command takes one or two argument."), ARG_ERROR);
    return executeRegister(words[1], words.count()==3 ? words[2] : TQString());
  }
  if ( words[0]=="break" ) {
    if ( words.count()==3 ) {
      if ( words[1]=="e" ) {
        bool ok;
        ulong address = fromAnyLabel(words[2], &ok);
        if ( !ok ) return errorExit(i18n("Number format not recognized."), ARG_ERROR);
        PURL::Url dummy;
        Breakpoint::Data data(dummy, address);
        if ( Breakpoint::list().contains(data) ) return okExit(i18n("Breakpoint already set at %1.").arg(toHexLabel(address, nbChars(NumberBase::Hex, address))));
        Breakpoint::list().append(data);
        Breakpoint::list().setAddress(data, address);
        Breakpoint::list().setState(data, Breakpoint::Active);
        return okExit(i18n("Breakpoint set at %1.").arg(toHexLabel(address, nbChars(NumberBase::Hex, address))));
      }
      return errorExit(i18n("The first argument should be \"e\"."), ARG_ERROR);
    }
    if ( words.count()==1 ) {
      uint nb = Breakpoint::list().count();
      if ( nb==0 ) return okExit(i18n("No breakpoint set."));
      log(Log::LineType::Normal, i18n("Breakpoints:"));
      uint nbc = 0;
      if (_device) nbc = _device->nbCharsAddress();
      else for (uint i=0; i<nb; i++) {
        Address address = Breakpoint::list().address(Breakpoint::list().data(i));
        nbc = TQMAX(nbc, nbChars(NumberBase::Hex, address.toUInt()));
      }
      for (uint i=0; i<nb; i++) {
        Address address = Breakpoint::list().address(Breakpoint::list().data(i));
        log(Log::LineType::Normal, TQString("  #%1: %2").arg(i).arg(toHexLabel(address, nbc)));
      }
      return OK;
    }
    if ( words.count()!=1 && words.count()!=3 ) return errorExit(i18n("This command takes no or two argument."), ARG_ERROR);
    return errorExit(i18n("Arguments not recognized."), ARG_ERROR);
  }
  if ( words[0]=="clear" ) {
    if ( words.count()!=2 ) return errorExit(i18n("This command takes one argument."), ARG_ERROR);
    if ( words[1]=="all" ) {
      Breakpoint::list().clear();
      return okExit(i18n("All breakpoints removed."));
    }
    bool ok;
    uint i = words[1].toUInt(&ok);
    if ( !ok ) return errorExit(i18n("Argument should be breakpoint index."), ARG_ERROR);
    if ( i>=Breakpoint::list().count() ) return errorExit(i18n("Breakpoint index too large."), ARG_ERROR);
    Breakpoint::Data data = Breakpoint::list().data(i);
    Address address = Breakpoint::list().address(data);
    Breakpoint::list().remove(data);
    return okExit(i18n("Breakpoint at %1 removed.").arg(toHexLabelAbs(address)));
  }
  if ( words[0]=="raw-com" ) {
    if ( words.count()!=2 ) return errorExit(i18n("This command needs a commands filename."), ARG_ERROR);
  } else if ( words[0]=="program" || words[0]=="read" || words[0]=="verify" || words[0]=="erase" || words[0]=="blank_check" ) {
    uint hexi = 1;
    if ( words.count()>=2 && words[1]=="-r" ) {
      hexi = 3;
      if ( words.count()==2 ) return errorExit(i18n("You need to specify the range."), ARG_ERROR);
      ExitCode code = static_cast<Main *>(_main)->extractRange(words[2]);
      if ( code!=OK ) return code;
    }
    if ( data->properties & (InputHex|OutputHex) ) {
      if ( uint(words.count())>(hexi+1) ) return errorExit(i18n("Too many arguments."), ARG_ERROR);
      if ( uint(words.count())==(hexi+1) )_hexUrl = PURL::Url(runDirectory(), words[hexi]);
      if ( _hexUrl.isEmpty() ) return errorExit(i18n("This command needs a hex filename."), ARG_ERROR);
    } else if ( uint(words.count())>hexi ) return errorExit(i18n("Too many arguments."), ARG_ERROR);
  } else if ( words.count()!=1 ) return errorExit(i18n("This command takes no argument."), ARG_ERROR);

  ExitCode code = static_cast<Main *>(_main)->prepareCommand(words[0]);
  if ( code!=OK ) return code;
  if ( words[0]=="raw-com" ) return executeRawCommands(words[1]);
  return static_cast<Main *>(_main)->executeCommand(words[0]);
}

void CLI::Interactive::lineRead()
{
  TQString s = _input.stripWhiteSpace();
  _input = TQString();
  if ( processLine(s)==EXITING ) {
    tqApp->exit(OK);
    return;
  }
  TQTimer::singleShot(0, this, TQ_SLOT(displayPrompt()));
}

CLI::ExitCode CLI::Interactive::registerList()
{
  if ( _device==0 ) return errorExit(i18n("No device specified."), NOT_SUPPORTED_ERROR);
  if ( _device->group().name()!="pic" ) return errorExit(i18n("No register."), NOT_SUPPORTED_ERROR);
  const Pic::Data &data = static_cast<const Pic::Data &>(*_device);
  const Coff::Object *coff = Debugger::manager->coff();
  const Pic::RegistersData &rdata = data.registersData();
  log(Log::LineType::Normal, i18n("Special Function Registers:"));
  TQValueVector<Pic::RegisterNameData> list = Pic::sfrList(data);
  for (uint i=0; i<uint(list.count()); i++) log(Log::LineType::Normal, TQString("  %1: %2").arg(toHexLabel(list[i].data().address(), rdata.nbCharsAddress())).arg(list[i].label()));
  log(Log::LineType::Normal, i18n("General Purpose Registers:"));
  list = Pic::gprList(data, coff);
  for (uint i=0; i<uint(list.count()); i++) log(Log::LineType::Normal, TQString("  %1: %2").arg(toHexLabel(list[i].data().address(), rdata.nbCharsAddress())).arg(list[i].label()));
  return OK;
}

CLI::ExitCode CLI::Interactive::variableList()
{
  if ( _device==0 ) return errorExit(i18n("No device specified."), NOT_SUPPORTED_ERROR);
  const Coff::Object *coff = Debugger::manager->coff();
  if ( coff==0 ) return errorExit(i18n("No COFF file specified."), NOT_SUPPORTED_ERROR);
  if ( _device->group().name()!="pic" ) return errorExit(i18n("No register."), NOT_SUPPORTED_ERROR);
  const Pic::Data &data = static_cast<const Pic::Data &>(*_device);
  const Pic::RegistersData &rdata = data.registersData();
  TQValueVector<Pic::RegisterNameData> list = Pic::variableList(data, *coff);
  if ( list.count()==0 ) log(Log::LineType::Normal, i18n("No variable."));
  for (uint i=0; i<uint(list.count()); i++) log(Log::LineType::Normal, TQString("  %1: %2").arg(toHexLabel(list[i].data().address(), rdata.nbCharsAddress())).arg(list[i].label()));
  return OK;
}

Address CLI::Interactive::findRegisterAddress(const TQString &name)
{
  const Pic::Data &data = static_cast<const Pic::Data &>(*_device);
  const Coff::Object *coff = Debugger::manager->coff();
  const Pic::RegistersData &rdata = data.registersData();
  bool ok;
  Address address = fromAnyLabel(name, &ok);
  if (ok) {
    if ( address>rdata.addressFromIndex(rdata.nbRegisters()-1) ) return Address();
    return address;
  }
  TQValueVector<Pic::RegisterNameData> sfrs = Pic::sfrList(data);
  for (uint i=0; i<uint(sfrs.count()); i++) if ( name.lower()==sfrs[i].label().lower() ) return sfrs[i].data().address();
  if ( coff==0 ) return Address();
  TQMap<TQString, Address> variables = coff->variables();
  if ( variables.contains(name) ) return variables[name];
  return Address();
}

CLI::ExitCode CLI::Interactive::executeRegister(const TQString &name, const TQString &value)
{
  if ( Debugger::manager->debugger()==0 ) return errorExit(i18n("You need to start the debugging session first (with \"start\")."), ARG_ERROR);
  const Pic::Data &data = static_cast<const Pic::Data &>(*_device);
  const Pic::RegistersData &rdata = data.registersData();
  uint nbChars = rdata.nbChars();
  bool ok;
  ulong v = fromAnyLabel(value, &ok);
  if ( !ok ) return errorExit(i18n("Number format not recognized."), ARG_ERROR);
  if ( v>maxValue(NumberBase::Hex, nbChars) ) return errorExit(i18n("The given value is too large (max: %1).").arg(toHexLabel(maxValue(NumberBase::Hex, nbChars), nbChars)), ARG_ERROR);
  Register::TypeData rtd;
  if ( name.lower()=="w" || name.lower()=="wreg" )
    rtd = static_cast<Debugger::PicBase *>(Debugger::manager->debugger())->deviceSpecific()->wregTypeData();
  else if ( name.lower()=="pc" )
    rtd = Debugger::manager->debugger()->pcTypeData();
  else {
    Address address = findRegisterAddress(name);
    if ( !address.isValid() ) return errorExit(i18n("Unknown register or variable name."), ARG_ERROR);
    rtd = Register::TypeData(address, rdata.nbChars());
  }
  if ( value.isEmpty() ) {
    if ( !Debugger::manager->readRegister(rtd) ) return ARG_ERROR;
    return okExit(i18n("%1 = %2").arg(name.upper()).arg(toHexLabel(Register::list().value(rtd), nbChars)));
  }
  return (Debugger::manager->writeRegister(rtd, v) ? OK : EXEC_ERROR);
}

CLI::ExitCode CLI::Interactive::executeRawCommands(const TQString &filename)
{
  TQFile file(filename);
  if ( !file.open(IO_ReadOnly) ) return errorExit(i18n("Could not open filename \"%1\".").arg(filename), ARG_ERROR);
  if ( Programmer::manager->programmer()==0 ) {
    Programmer::manager->createProgrammer(_device);
    if ( !Programmer::manager->programmer()->simpleConnectHardware() ) return EXEC_ERROR;
  }
  Programmer::Base *programmer = Programmer::manager->programmer();
  for (;;) {
    TQString s;
    if ( file.readLine(s, 1000)==-1 ) break;
    bool write = !s.startsWith(" ");
    s = s.stripWhiteSpace();
    if ( s.isEmpty() ) continue;
    if (write) {
      if ( !programmer->hardware()->rawWrite(s) ) return EXEC_ERROR;
    } else {
      TQString rs;
      if ( !programmer->hardware()->rawRead(s.length(), rs) ) return EXEC_ERROR;
      if ( rs!=s ) log(Log::LineType::Warning, i18n("Read string is different than expected: %1 (%2).").arg(rs).arg(s));
    }
  }
  return okExit(i18n("End of command file reached."));
}

#include "cli_interactive.moc"
