/***************************************************************************
 * Copyright (C) 2007 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 "pickit2v2.h"

#include "progs/base/prog_config.h"
#include "progs/icd2/base/microchip.h"
#include "pickit2v2_data.h"

//-----------------------------------------------------------------------------
const Pickit2V2::FamilyData *Pickit2V2::familyData(const Pic::Data &device)
{
  for (uint i=0; FAMILY_DATA[i].architecture!=Pic::Architecture::Nb_Types; i++)
    if ( FAMILY_DATA[i].architecture==device.architecture() ) return &FAMILY_DATA[i];
  Q_ASSERT(false);
  return 0;
}

//-----------------------------------------------------------------------------
Pickit2V2::Hardware::Hardware(::Programmer::Base &base)
  : ::Programmer::PicHardware(base, new USBPort(base), TQString()),
    _scriptBufferChecksum(0), _deviceSet(false)
{}

bool Pickit2V2::Hardware::internalConnectHardware()
{
  return port().open();
}

bool Pickit2V2::Hardware::setTarget()
{
  //setIcspSpeed(0); // #### ??
  _fastProgramming = true; // #### ??
  uint checksum;
  if ( !getScriptBufferChecksum(checksum) ) return false;
  if ( !_deviceSet || _scriptBufferChecksum!=checksum ) {
    if ( !downloadScripts() ) return false;
  }
  _deviceSet = true;
  return true;
}

bool Pickit2V2::Hardware::readStatus(ushort &status)
{
  if ( !port().command(ReadStatus) ) return false;
  Array a;
  if ( !port().receive(a) ) return false;
  status = (a[1] << 8) + a[0];
  return true;
}

bool Pickit2V2::Hardware::sendScript(const ushort *script, uint length)
{
  Array cmd;
  cmd[0] = ClearUploadBuffer;
  cmd[1] = ExecuteScript;
  cmd[2] = length;
  for (uint i=0; i<length; i++) cmd[3+i] = script[i];
  return port().command(cmd);
}

bool Pickit2V2::Hardware::executeScript(uint i)
{
  Q_ASSERT( i!=0 );
  const ScriptData &sdata = SCRIPT_DATA[i-1];
  log(Log::DebugLevel::Extra, TQString("execute script #%1: %2").arg(i).arg(sdata.name));
  return sendScript(sdata.data, sdata.length);
}

bool Pickit2V2::Hardware::getScriptBufferChecksum(uint &checksum)
{
  if ( !port().command(ScriptBufferChecksum) ) return false;
  Array array;
  if ( !port().receive(array) ) return false;
  checksum = (array[0] << 24) + (array[1] << 16) + (array[2] << 8) + array[3];
  log(Log::DebugLevel::Extra, TQString("get script buffer checksum: %1").arg(toHexLabel(checksum, 8)));
  return true;
}

bool Pickit2V2::Hardware::downloadScript(ScriptType type, uint i)
{
  if (i==0 ) return true; // empty script
  const ScriptData &sdata = SCRIPT_DATA[i-1];
  log(Log::DebugLevel::Max, TQString("  download script #%1 (\"%2\") at position #%3")
                            .arg(i-1).arg(sdata.name).arg(toHexLabel(type, 2)));
  Array cmd;
  cmd[0] = DownloadScript;
  cmd[1] = type;
  cmd[2] = sdata.length;
  for (uint k=0; k<sdata.length; k++) {
    if ( !_fastProgramming && sdata.data[k]==0xAAE7 ) {
      ushort next = sdata.data[k+1];
      if ( (next & 0xFF)<170 && next!=0 ) {
        cmd[3+k] = sdata.data[k];
        cmd[3+k+1] = next + next/2;
      } else {
        cmd[3+k] = DelayLong;
        cmd[3+k+1] = 2;
      }
      k++;
    } else if ( !_fastProgramming && sdata.data[k]==0xAAE8 ) {
      ushort next = sdata.data[k+1];
      if ( (next & 0xFF)<171 && next!=0 ) {
        cmd[3+k] = sdata.data[k];
        cmd[3+k+1] = next + next/2;
      } else {
        cmd[3+k] = DelayLong;
        cmd[3+k+1] = 0;
      }
      k++;
    } else cmd[3+k] = sdata.data[k];
  }
  return port().command(cmd);
}

bool Pickit2V2::Hardware::downloadScripts()
{
  if ( !port().command(ClearDownloadBuffer) ) return false;
  log(Log::DebugLevel::Extra, "clear scripts buffer");
  if ( !port().command(ClearScriptBuffer) ) return false;
  log(Log::DebugLevel::Extra, "download scripts");
  const Data &d = data(device().name());
  for (uint i=0; i<Nb_ScriptTypes; i++) {
    if ( i==TestMemoryRead || i==ConfigErase ) continue; // ### to get same checksum as pk2cmd
    if ( !downloadScript(ScriptType(i), d.scriptIndexes[i]) ) return false;
  }
  return getScriptBufferChecksum(_scriptBufferChecksum);
}

bool Pickit2V2::Hardware::setTargetReset(Pic::ResetMode mode)
{
  ushort script[1];
  switch (mode) {
    case Pic::ResetHeld: script[0] = MclrGroundOn; break;
    case Pic::ResetReleased: script[0] = MclrGroundOff; break;
    case Pic::Nb_ResetModes: Q_ASSERT(false); return false;
  }
  return sendScript(script, 1);
}

bool Pickit2V2::Hardware::setVddVoltage(double value, double threshold)
{
  ushort v = ushort(tqRound(32.0*value + 10.5)) << 6;
  uchar vfault = uchar(tqRound(256.0 * (threshold * value) / 5.0));
  if ( vfault>210 ) vfault = 210;
  Array cmd;
  cmd[0] = SetVdd;
  cmd[1] = v & 0xFF;
  cmd[2] = v >> 8;
  cmd[3] = vfault;
  return port().command(cmd);
}

bool Pickit2V2::Hardware::setVppVoltage(double value, double threshold)
{
  Array cmd;
  cmd[0] = SetVpp;
  cmd[1] = 0x40;
  cmd[2] = uchar(tqRound(18.61*value));
  cmd[3] = uchar(tqRound(18.61*value*threshold));
  return port().command(cmd);
}

bool Pickit2V2::Hardware::setVddOn(bool on)
{
  log(Log::DebugLevel::Extra, TQString("Vdd set to %1, self powered is %2").arg(on).arg(_base.isTargetSelfPowered()));
  ushort script[2];
  script[0] = (on ? VddGroundOff : VddOff);
  if ( _base.isTargetSelfPowered() ) script[1] = (on ? VddOff : VddGroundOff);
  else script[1] = (on ? VddOn : VddGroundOn);
  return sendScript(script, 2);
}

bool Pickit2V2::Hardware::setIcspSpeed(uchar speed)
{
  ushort script[2];
  script[0] = SetIcspSpeed;
  script[1] = speed;
  return sendScript(script, 2);
}

bool Pickit2V2::Hardware::getMode(VersionData &version, ::Programmer::Mode &mode)
{
  if ( !port().command(FirmwareVersion) ) return false;
  Array data;
  if ( !port().receive(data) ) return false;
  if ( data[0]=='B' ) {
    mode = ::Programmer::BootloadMode;
    version = VersionData();
  } else {
    mode = ::Programmer::NormalMode;
    version = VersionData(data[0], data[1], data[2]);
  }
  return true;
}

bool Pickit2V2::Hardware::readVoltages(VoltagesData &voltagesData)
{
  if ( !port().command(ReadVoltages) ) return false;
  Array array;
  if ( !port().receive(array) ) return false;
  double vadc = 256 * array[1] + array[0];
  voltagesData[Pic::TargetVdd].value = 5.0 * (vadc / 65536);
  voltagesData[Pic::TargetVdd].error = false;
  vadc = 256 * array[3] + array[2];
  voltagesData[Pic::TargetVpp].value = 13.7 * (vadc / 65536);
  voltagesData[Pic::TargetVpp].error = false;
  return true;
}

bool Pickit2V2::Hardware::downloadAddress(Address address)
{
  log(Log::DebugLevel::Max, TQString("download address %1").arg(toHexLabel(address, 6)));
  Array cmd;
  cmd[0] = ClearDownloadBuffer;
  cmd[1] = DownloadData;
  cmd[2] = 3;
  cmd[3] = address.toUInt() & 0xFF;
  cmd[4] = (address.toUInt() >> 8) & 0xFF;
  cmd[5] = (address.toUInt() >> 16) & 0xFF;
  return port().command(cmd);
}

bool Pickit2V2::Hardware::runScript(ScriptType stype, uint repetitions, uint nbNoLens)
{
  log(Log::DebugLevel::Max, TQString("run script %1: repetitions=%2 nbNoLen=%3")
                            .arg(toHexLabel(stype, 2)).arg(repetitions).arg(nbNoLens));
#if 0 // ALTERNATE METHOD
  const Data &d = data(device().name());
  for (uint i=0; i<repetitions; i++)
    if ( !executeScript(d.scriptIndexes[stype]) ) return false;
  if (nbNoLens) {
    Array cmd;
    for (uint i=0; i<nbNoLens; i++) cmd[i] = UploadDataNoLen;
    if ( !port().command(cmd) ) return false;
  }
#else
  Array cmd;
  cmd[0] = ClearUploadBuffer;
  cmd[1] = RunScript;
  cmd[2] = stype;
  cmd[3] = repetitions;
  for (uint i=0; i<nbNoLens; i++) cmd[4+i] = UploadDataNoLen;
  if ( !port().command(cmd) ) return false;
#endif
  if ( stype==ProgExit && !setTargetReset(Pic::ResetReleased) ) return false;
  return true;
}

bool Pickit2V2::Hardware::prepareRead(Pic::MemoryRangeType type, uint wordIndex)
{
  ScriptType stype = prepareReadScript(type);
  if ( type!=Pic::MemoryRangeType::Cal && (stype==Nb_ScriptTypes || data(device().name()).scriptIndexes[stype]==0) ) return true;
  switch (type.type()) {
  case Pic::MemoryRangeType::Code:
    if ( !device().architecture().data().hasAddressAccess ) return true;
    if ( !downloadAddress(0x10000 * (wordIndex / 0x8000)) ) return false;
    break;
  case Pic::MemoryRangeType::Eeprom:
    if ( device().nbBytesWord(Pic::MemoryRangeType::Eeprom)==4 ) {
      if ( !downloadAddress(device().range(Pic::MemoryRangeType::Eeprom).start.toUInt()) ) return false; // #### correct ?
    } else if ( !downloadAddress(0x0) ) return false;
    break;
  case Pic::MemoryRangeType::Config: return true; // ConfigPrepareRead unused (?)
  case Pic::MemoryRangeType::UserId: break;
  case Pic::MemoryRangeType::Cal: {
    Address start = device().range(Pic::MemoryRangeType::Cal).start;
    Q_ASSERT( start==device().range(Pic::MemoryRangeType::Code).end+1 );
    return downloadAddress(start.toUInt());
  }
  default: return true;
  case Pic::MemoryRangeType::Nb_Types: Q_ASSERT(false); return false;
  }
  return runScript(stype);
}

bool Pickit2V2::Hardware::readMemory(Pic::MemoryRangeType otype, Device::Array &data, const ::Programmer::VerifyData *vdata)
{
  uint nbWords = device().nbWords(otype);
  data.resize(nbWords);
  log(Log::DebugLevel::Max, TQString("read %1 nbWords=%2").arg(otype.label()).arg(nbWords));
  uint nbBytesWord = device().nbBytesWord(otype);
  // EEPROM is read like regular program memory in midrange
  if ( !device().is18Family() && !device().is16bitFamily() && otype==Pic::MemoryRangeType::Eeprom ) nbBytesWord = 2;
  uint wordOffset = 0;
  Pic::MemoryRangeType type = otype;
  if ( otype==Pic::MemoryRangeType::Config && device().range(Pic::MemoryRangeType::Config).start==device().range(Pic::MemoryRangeType::Code).end+1 ) {  // config in code memory
    wordOffset = device().range(Pic::MemoryRangeType::Config).start.toUInt();
    type = Pic::MemoryRangeType::Code;
  }
  bool setAddress = true;
  ScriptType stype = readScript(type);
  Q_ASSERT( stype!=Nb_ScriptTypes );
  const FamilyData *fdata = familyData(device());
  uint nbRunWords = TQMIN(UploadBufferNbBytes / nbBytesWord, nbWords);
  uint nbRuns = 1;
  uint nbReceive = (nbRunWords*nbBytesWord + 63) / 64;
  switch (type.type()) {
    case Pic::MemoryRangeType::Code:   nbRuns = nbRunWords / Pickit2V2::data(device().name()).codeMemoryNbReadWords; break;
    case Pic::MemoryRangeType::Eeprom: nbRuns = nbRunWords / Pickit2V2::data(device().name()).eepromMemoryNbReadWords; break;
    default: break;
  }

  if ( !runScript(ProgEntry) ) return false;
  for (uint i=0; i<nbWords; ) {
    if (setAddress) {
      setAddress = false;
      if ( !prepareRead(type, wordOffset + i) ) return false;
    }
    TQValueVector<uint> words;
    if ( type==Pic::MemoryRangeType::Config || type==Pic::MemoryRangeType::Cal ) {
      if ( !runScript(stype, 1, 0) ) return false;
      if ( !port().command(UploadData) ) return false;
      // config memory return includes a length byte that we need to ignore
      if ( !port().receiveWords(nbBytesWord, nbReceive, words, 1) ) return false;
    } else {
      if ( !runScript(stype, nbRuns, nbReceive) ) return false;
      if ( !port().receiveWords(nbBytesWord, nbReceive, words) ) return false;
    }
    log(Log::DebugLevel::Max, TQString("nbRunWords=%1 readNbWords=%2 index=%3/%4").arg(nbRunWords).arg(words.count()).arg(i).arg(nbWords));
    for (uint k=0; k<words.count(); k++) {
      if ( i>=nbWords ) break;
      data[i] = words[k];
      if (fdata->progMemShift) data[i] >>= 1;
      data[i] = data[i].maskWith(device().mask(type)); // ### correct ?
      if ( vdata && !verifyWord(i, data[i], type, *vdata) ) return false;
      if ( type==Pic::MemoryRangeType::Code && i!=0x0 && i%0x8000==0 ) setAddress = true;
      i++;
    }
  }
  if ( !runScript(ProgExit) ) return false;
  return true;
}

bool Pickit2V2::Hardware::eraseAll()
{
  const Data &d = data(device().name());
  if ( d.scriptIndexes[ConfigErase]!=0 ) {
    if ( !runScript(ProgEntry) ) return false;
    if ( d.scriptIndexes[ConfigWritePrepare]!=0 ) {
      if ( !downloadAddress(0) ) return false;
      if ( !runScript(ConfigWritePrepare) ) return false;
    }
    if ( !runScript(ConfigErase) ) return false;
    if ( !runScript(ProgExit) ) return false;
  }
  if ( !runScript(ProgEntry) ) return false;
  if ( d.scriptIndexes[EraseChipPrepare]!=0 && !runScript(EraseChipPrepare) ) return false;
  if ( !runScript(ChipErase) ) return false;
  if ( !runScript(ProgExit) ) return false;
  return true;
}

bool Pickit2V2::Hardware::eraseRange(Pic::MemoryRangeType type)
{
  if ( type==Pic::MemoryRangeType::Code ) {
    if ( !runScript(ProgEntry) ) return false;
    if ( !runScript(ProgMemoryErase) ) return false;
    if ( !runScript(ProgExit) ) return false;
    return true;
  }
  if ( type==Pic::MemoryRangeType::Eeprom ) {
    if ( !runScript(ProgEntry) ) return false;
    if ( !runScript(EepromErase) ) return false;
    if ( !runScript(ProgExit) ) return false;
    return true;
  }
  Q_ASSERT(false);
  return false;
}

bool Pickit2V2::Hardware::prepareWrite(Pic::MemoryRangeType type, uint wordIndex)
{
  ScriptType stype = prepareWriteScript(type);
  if ( stype==Nb_ScriptTypes || data(device().name()).scriptIndexes[stype]==0 ) return true;
  switch (type.type()) {
  case Pic::MemoryRangeType::Code:
  case Pic::MemoryRangeType::Config:
    if ( !device().architecture().data().hasAddressAccess ) return true;
    if ( !downloadAddress(0x10000 * (wordIndex / 0x8000)) ) return false;
    break;
  case Pic::MemoryRangeType::Eeprom: {
    Address address = (device().nbBytesWord(Pic::MemoryRangeType::Eeprom)==4 ? device().range(Pic::MemoryRangeType::Eeprom).start : 0x000000);
    if ( !downloadAddress(address) ) return false;
    break;
  }
  case Pic::MemoryRangeType::UserId: break;
  default: return true;
  case Pic::MemoryRangeType::Nb_Types: Q_ASSERT(false); return false;
  }
  return runScript(stype);
}

bool Pickit2V2::Hardware::writeMemory(Pic::MemoryRangeType otype, const Device::Array &data, bool force)
{
  Q_UNUSED(force);
  return false; // ### TODO
}

//-----------------------------------------------------------------------------
bool Pickit2V2::DeviceSpecific::canEraseRange(Pic::MemoryRangeType type) const
{
  const Data &d = data(device().name());
  if ( type==Pic::MemoryRangeType::Code ) return d.scriptIndexes[ProgMemoryErase];
  if ( type==Pic::MemoryRangeType::Eeprom ) return d.scriptIndexes[EepromErase];
  return false;
}

bool Pickit2V2::DeviceSpecific::canReadRange(Pic::MemoryRangeType type) const
{
  const Data &d = data(device().name());
  if ( type==Pic::MemoryRangeType::Cal ) return d.scriptIndexes[OsccalRead];
  return true;
}

bool Pickit2V2::DeviceSpecific::canWriteRange(Pic::MemoryRangeType type) const
{
  const Data &d = data(device().name());
  if ( type==Pic::MemoryRangeType::Cal ) return d.scriptIndexes[OsccalWrite];
  return true;
}

bool Pickit2V2::DeviceSpecific::doWrite(Pic::MemoryRangeType type, const Device::Array &data, bool force)
{
  Q_ASSERT( data.size()==device().nbWords(type) );
  return hardware().writeMemory(type, data, force);
}
