/* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only                     */
#include "stdinc.h"
#include "nlmessages.h"
#include "stringlists.h"
#include "codechunks.h"
#include "entryaddress.h"
#include "invaddress.h"
#include "strutil.h"
#include "cmdarg.h"
#include "msg_level.h"
#include "dasmdef.h"
#include "cpulist.h"
#include "console.h"
#include "nls.h"
#include "version.h"
#include "das.rsc"
#ifdef _USE_MSH
# include "das.msh"
#endif

#include "deco68.h"
#include "deco87c800.h"
#include "deco4004.h"

#define TABSIZE 8

char *pEnvName = "DASCMD";

void WrCopyRight(const char *Msg)
{
  printf("%s\n%s\n", Msg, InfoMessCopyright);
}

typedef void (*tChunkCallback)(const OneChunk *pChunk, Boolean IsData, void *pUser);

static void IterateChunks(tChunkCallback Callback, void *pUser)
{
  Word NextCodeChunk, NextDataChunk;
  const OneChunk *pChunk;
  Boolean IsData;

  NextCodeChunk = NextDataChunk = 0;
  while ((NextCodeChunk < UsedCodeChunks.RealLen) || (NextDataChunk < UsedDataChunks.RealLen))
  {
    if (NextCodeChunk >= UsedCodeChunks.RealLen)
    {
      pChunk = UsedDataChunks.Chunks + (NextDataChunk++);
      IsData = True;
    }
    else if (NextDataChunk >= UsedDataChunks.RealLen)
    {
      pChunk = UsedCodeChunks.Chunks + (NextCodeChunk++);
      IsData = False;
    }
    else if (UsedDataChunks.Chunks[NextDataChunk].Start < UsedCodeChunks.Chunks[NextCodeChunk].Start)
    {
      pChunk = UsedDataChunks.Chunks + (NextDataChunk++);
      IsData = True;
    }
    else
    {
      pChunk = UsedCodeChunks.Chunks + (NextCodeChunk++);
      IsData = False;
    }

    Callback(pChunk, IsData, pUser);
  }
}

typedef struct
{
  FILE *pDestFile;
  LargeWord Sum;
} tDumpIteratorData;

static void DumpIterator(const OneChunk *pChunk, Boolean IsData, void *pUser)
{
  String Str;
  tDumpIteratorData *pData = (tDumpIteratorData*)pUser;

  HexString(Str, sizeof(Str), pChunk->Start, 0);
  fprintf(pData->pDestFile, "\t\t; %s...", Str);
  HexString(Str, sizeof(Str), pChunk->Start + pChunk->Length - 1, 0);
  fprintf(pData->pDestFile, "%s (%s)\n", Str, IsData ? "data" :"code");
  pData->Sum += pChunk->Length;
}

static void DumpChunks(const ChunkList *NChunk, FILE *pDestFile)
{
  tDumpIteratorData Data;
  String Str;

  UNUSED(NChunk);

  Data.pDestFile = pDestFile;
  Data.Sum = 0;
  fprintf(pDestFile, "\t\t; disassembled area:\n");
  IterateChunks(DumpIterator, &Data);
  as_snprintf(Str, sizeof(Str), "\t\t; %lllu/%lllu bytes disassembled", Data.Sum, GetCodeChunksStored(&CodeChunks));
  fprintf(pDestFile, "%s\n", Str);
}

static int tabbedstrlen(const char *s)
{
  int Result = 0;

  for (; *s; s++)
  {
    if (*s == '\t')
      Result += TABSIZE - (Result % TABSIZE);
    else
      Result++;
  }
  return Result;
}

static void PrTabs(FILE *pDestFile, int TargetLen, int ThisLen)
{
  while (ThisLen < TargetLen)
  {
    fputc('\t', pDestFile);
    ThisLen += TABSIZE - (ThisLen % TABSIZE);
  }
}

static as_cmd_result_t ArgError(int MsgNum, const char *pArg)
{
  if (pArg)
    fprintf(stderr, "%s:", pArg);
  fprintf(stderr, "%s\n", getmessage(MsgNum));

  return e_cmd_err;
}

static as_cmd_result_t CMD_BinFile(Boolean Negate, const char *pArg)
{
  LargeWord Start = 0, Len = 0, Gran = 1;
  char *pStart = NULL, *pLen = NULL, *pGran = NULL, *p_end;
  String Arg;
  tCodeChunk Chunk;

  if (Negate || !*pArg)
    return ArgError(Num_ErrMsgFileArgumentMissing, NULL);

  strmaxcpy(Arg, pArg, sizeof(Arg));
  if ((pStart = strchr(Arg, '@')))
  {
    *pStart++ = '\0';
    if ((pLen = strchr(pStart, ',')))
    {
      *pLen++ = '\0';
      if ((pGran = strchr(pLen, ',')))
        *pGran++ = '\0';
    }
  }

  if (pStart && *pStart)
  {
    Start = strtoul(pStart, &p_end, 10);
    if (*p_end)
      return ArgError(Num_ErrMsgInvalidNumericValue, pStart);
  }
  else
    Start = 0;

  if (pLen && *pLen)
  {
    Len = strtoul(pLen, &p_end, 10);
    if (*p_end)
      return ArgError(Num_ErrMsgInvalidNumericValue, pLen);
  }
  else
    Len = 0;

  if (pGran && *pGran)
  {
    Gran = strtoul(pGran, &p_end, 10);
    if (*p_end)
      return ArgError(Num_ErrMsgInvalidNumericValue, pGran);
  }
  else
    Gran = 1;

  InitCodeChunk(&Chunk);
  if (ReadCodeChunk(&Chunk, Arg, Start, Len, Gran))
    return ArgError(Num_ErrMsgCannotReadBinaryFile, Arg);
  MoveCodeChunkToList(&CodeChunks, &Chunk, TRUE);

  return e_cmd_arg;
}

static void ResizeBuffer(Byte* *ppBuffer, LargeWord *pAllocLen, LargeWord ReqLen)
{
  if (ReqLen > *pAllocLen)
  {
    Byte *pNew = *ppBuffer ? realloc(*ppBuffer, ReqLen) : malloc(ReqLen);
    if (pNew)
    {
      *ppBuffer = pNew;
      *pAllocLen = ReqLen;
    }
  }
}

static Boolean GetByte(char* *ppLine, Byte *pResult)
{
  if (!as_isxdigit(**ppLine))
    return False;
  *pResult = isdigit(**ppLine) ? (**ppLine - '0') : (as_toupper(**ppLine) - 'A' + 10);
  (*ppLine)++;
  if (!as_isxdigit(**ppLine))
    return False;
  *pResult = (*pResult << 4) | (isdigit(**ppLine) ? (**ppLine - '0') : (as_toupper(**ppLine) - 'A' + 10));
  (*ppLine)++;
  return True;
}

static void FlushChunk(tCodeChunk *pChunk)
{
  pChunk->Granularity = 1;
  pChunk->pLongCode = (LongWord*)pChunk->pCode;
  pChunk->pWordCode = (Word*)pChunk->pCode;
  MoveCodeChunkToList(&CodeChunks, pChunk, TRUE);
  InitCodeChunk(pChunk);
}

/* ------------------------------------------------------- */

static Boolean write_version_exit, write_help_exit, write_cpu_list_exit;

static int screen_height = 0;

static void write_console_next(const char *p_line)
{
  static int LineZ;

  WrConsoleLine(p_line, True);
  if (screen_height && (++LineZ >= screen_height))
  {
    LineZ = 0;
    WrConsoleLine(getmessage(Num_KeyWaitMsg), False);
    fflush(stdout);
    while (getchar() != '\n');
  }
}

static as_cmd_result_t CMD_HexFile(Boolean Negate, const char *pArg)
{
  FILE *pFile;
  char Line[300], *pLine;
  size_t Len;
  Byte *pLineBuffer = NULL, *pDataBuffer = 0, RecordType, Tmp;
  LargeWord LineBufferStart = 0, LineBufferAllocLen = 0, LineBufferLen = 0,
            Sum;
  tCodeChunk Chunk;
  unsigned z;

  if (Negate || !*pArg)
    return ArgError(Num_ErrMsgFileArgumentMissing, NULL);

  pFile = fopen(pArg, "r");
  if (!pFile)
  {
    return ArgError(Num_ErrMsgCannotReadHexFile, pArg);
  }

  InitCodeChunk(&Chunk);
  while (!feof(pFile))
  {
    fgets(Line, sizeof(Line), pFile);
    Len = strlen(Line);
    if ((Len > 0) && (Line[Len -1] == '\n'))
      Line[--Len] = '\0';
    if ((Len > 0) && (Line[Len -1] == '\r'))
      Line[--Len] = '\0';
    if (*Line != ':')
      continue;

    Sum = 0;
    pLine = Line + 1;
    if (!GetByte(&pLine, &Tmp))
      return ArgError(Num_ErrMsgInvalidHexData, pArg);
    ResizeBuffer(&pLineBuffer, &LineBufferAllocLen, Tmp);
    LineBufferLen = Tmp;
    Sum += Tmp;

    LineBufferStart = 0;
    for (z = 0; z < 2; z++)
    {
      if (!GetByte(&pLine, &Tmp))
        return ArgError(Num_ErrMsgInvalidHexData, pArg);
      LineBufferStart = (LineBufferStart << 8) | Tmp;
      Sum += Tmp;
    }

    if (!GetByte(&pLine, &RecordType))
      return ArgError(Num_ErrMsgInvalidHexData, pArg);
    Sum += RecordType;
    if (RecordType != 0)
      continue;

    for (z = 0; z < LineBufferLen; z++)
    {
      if (!GetByte(&pLine, &pLineBuffer[z]))
        return ArgError(Num_ErrMsgInvalidHexData, pArg);
      Sum += pLineBuffer[z];
    }

    if (!GetByte(&pLine, &Tmp))
      return ArgError(Num_ErrMsgInvalidHexData, pArg);
    Sum += Tmp;
    if (Sum & 0xff)
      return ArgError(Num_ErrMsgHexDataChecksumError, pArg);

    if (Chunk.Start + Chunk.Length == LineBufferStart)
    {
      ResizeBuffer(&Chunk.pCode, &Chunk.Length, Chunk.Length + LineBufferLen);
      memcpy(&Chunk.pCode[Chunk.Length - LineBufferLen], pLineBuffer, LineBufferLen);
    }
    else
    {
      if (Chunk.Length)
        FlushChunk(&Chunk);
      ResizeBuffer(&Chunk.pCode, &Chunk.Length, LineBufferLen);
      memcpy(Chunk.pCode, pLineBuffer, LineBufferLen);
      Chunk.Start = LineBufferStart;
    }
  }
  if (Chunk.Length)
    FlushChunk(&Chunk);

  if (pLineBuffer)
    free(pLineBuffer);
  if (pDataBuffer)
    free(pDataBuffer);
  fclose(pFile);
  return e_cmd_ok;
}

static as_cmd_result_t CMD_EntryAddress(Boolean Negate, const char *pArg)
{
  LargeWord Address;
  char *pName = NULL, *p_end;
  String Arg, Str;

  if (Negate || !*pArg)
    return ArgError(Num_ErrMsgAddressArgumentMissing, NULL);

  strmaxcpy(Arg, pArg, sizeof(Arg));
  if ((pName = ParenthPos(Arg, ',')))
    *pName++ = '\0';

  if (*Arg)
  {
    if (*Arg == '(')
    {
      Byte Vector[8];
      char *pVectorAddress = NULL, *pAddrLen = NULL, *pEndianess = NULL;
      LargeWord AddrLen, VectorAddress = 0, z;
      Boolean VectorMSB;
      int l;

      pVectorAddress = Arg + 1;
      l = strlen(pVectorAddress);
      if (pVectorAddress[l - 1] != ')')
        return ArgError(Num_ErrMsgClosingPatentheseMissing, pVectorAddress);
      pVectorAddress[l - 1] = '\0';

      if ((pAddrLen = strchr(pVectorAddress, ',')))
      {
        *pAddrLen++ = '\0';
        if ((pEndianess = strchr(pAddrLen, ',')))
          *pEndianess++ = '\0';
      }

      if (pVectorAddress && *pVectorAddress)
      {
        VectorAddress = strtoul(pVectorAddress, &p_end, 10);
        if (*p_end)
          return ArgError(Num_ErrMsgInvalidNumericValue, pVectorAddress);
      }
      else
        pVectorAddress = 0;

      if (pAddrLen && *pAddrLen)
      {
        AddrLen = strtoul(pAddrLen, &p_end, 10);
        if (*p_end || (AddrLen > sizeof(Vector)))
          return ArgError(Num_ErrMsgInvalidNumericValue, pAddrLen);
      }
      else
        AddrLen = 1;

      if (pEndianess && *pEndianess)
      {
        if (!as_strcasecmp(pEndianess, "MSB"))
          VectorMSB = True;
        else if (!as_strcasecmp(pEndianess, "LSB"))
          VectorMSB = False;
        else
          return ArgError(Num_ErrMsgInvalidEndinaness, pEndianess);
      }
      else
        VectorMSB = True; /* TODO: depend on CPU */

      if (!RetrieveCodeFromChunkList(&CodeChunks, VectorAddress, Vector, AddrLen))
        return ArgError(Num_ErrMsgCannotRetrieveEntryAddressData, NULL);

      Address = 0;
      for (z = 0; z < AddrLen; z++)
      {
        Address <<= 8;
        Address |= VectorMSB ? Vector[z] : Vector[AddrLen - 1 - z];
      }
      as_snprintf(Str, sizeof Str, "indirect address @ %lllx -> 0x%lllx", VectorAddress, Address);
      printf("%s\n", Str);
      AddChunk(&UsedDataChunks, VectorAddress, AddrLen, True);

      if (pName && *pName)
      {
        String Str;

        as_snprintf(Str, sizeof(Str), "Vector_2_%s", pName);
        AddInvSymbol(Str, VectorAddress);
      }
    }
    else
    {
      Address = strtoul(pArg, &p_end, 10);
      if (*p_end)
        return ArgError(Num_ErrMsgInvalidNumericValue, pArg);
    }
  }
  else
    Address = 0;

  if (pName && *pName)
    AddInvSymbol(pName, Address);
  AddEntryAddress(Address);

  return e_cmd_arg;
}

static as_cmd_result_t CMD_Symbol(Boolean Negate, const char *pArg)
{
  LargeWord Address;
  char *pName = NULL, *p_end;
  String Arg;

  if (Negate || !*pArg)
    return ArgError(Num_ErrMsgSymbolArgumentMissing, NULL);

  strmaxcpy(Arg, pArg, sizeof(Arg));
  if ((pName = strchr(Arg, '=')))
    *pName++ = '\0';

  if (*Arg)
  {
    Address = strtoul(Arg, &p_end, 10);
    if (*p_end)
      return ArgError(Num_ErrMsgInvalidNumericValue, Arg);
  }
  else
    Address = 0;

  if (pName && *pName)
    AddInvSymbol(pName, Address);

  return e_cmd_arg;
}

static as_cmd_result_t CMD_CPU(Boolean Negate, const char *pArg)
{
  const tCPUDef *pCPUDef;

  if (Negate || !*pArg)
    return ArgError(Num_ErrMsgCPUArgumentMissing, NULL);

  if (!as_strcasecmp(pArg, "?") || !as_strcasecmp(pArg, "LIST"))
  {
    write_cpu_list_exit = True;
    return e_cmd_ok;
  }

  pCPUDef = LookupCPUDefByName(pArg);
  if (!pCPUDef)
    return ArgError(Num_ErrMsgUnknownCPU, pArg);

  pCPUDef->SwitchProc(pCPUDef->pUserData);

  return e_cmd_arg;
}

static as_cmd_result_t CMD_HexLowerCase(Boolean Negate, const char *Arg)
{
  UNUSED(Arg);

  HexStartCharacter = Negate ? 'A' : 'a';
  return e_cmd_ok;
}

static as_cmd_result_t CMD_PrintVersion(Boolean Negate, const char *Arg)
{
  UNUSED(Arg);

  if (Negate)
    return e_cmd_err;

  write_version_exit = True;
  return e_cmd_ok;
}

static as_cmd_result_t CMD_PrintHelp(Boolean Negate, const char *Arg)
{
  UNUSED(Arg);

  if (Negate)
    return e_cmd_err;

  write_help_exit = True;
  return e_cmd_ok;
}

static as_cmd_result_t CMD_screen_height(Boolean negate, const char *p_arg)
{
  char *p_end;
  int new_screen_height;

  if (negate)
  {
    screen_height = 0;
    return e_cmd_ok;
  }
  new_screen_height = strtoul(p_arg, &p_end, 10);
  if (*p_end)
    return e_cmd_err;
  screen_height = new_screen_height;
  return e_cmd_arg;
}

static as_cmd_rec_t DASParams[] =
{
  { "CPU"             , CMD_CPU             },
  { "BINFILE"         , CMD_BinFile         },
  { "HEXFILE"         , CMD_HexFile         },
  { "ENTRYADDRESS"    , CMD_EntryAddress    },
  { "SCREENHEIGHT"    , CMD_screen_height   },
  { "SYMBOL"          , CMD_Symbol          },
  { "h"               , CMD_HexLowerCase    },
  { "HELP"            , CMD_PrintHelp       },
  { "VERSION"         , CMD_PrintVersion    }
};

typedef struct
{
  FILE *pDestFile;
  int MaxSrcLineLen, MaxLabelLen;
} tDisasmData;

static void DisasmIterator(const OneChunk *pChunk, Boolean IsData, void *pUser)
{
  LargeWord Address;
  char NumString[50];
  tDisassInfo Info;
  tDisasmData *pData = (tDisasmData*)pUser;
  const char *pLabel;
  Byte Code[100];
  unsigned z;
  int DataSize = -1;

  Address = pChunk->Start;
  HexString(NumString, sizeof(NumString), Address, 0);
  fprintf(pData->pDestFile, "\n");
  PrTabs(pData->pDestFile, pData->MaxLabelLen, 0);
  fprintf(pData->pDestFile, "org\t$%s\n", NumString);
  while (Address < pChunk->Start + pChunk->Length)
  {
    pLabel = LookupInvSymbol(Address);
    if (pLabel && !strncmp(pLabel, "Vector_", 7) && IsData)
    {
      String Num;
      char *pEnd = strchr(pLabel + 7, '_');

      if (pEnd)
      {
        int l = pEnd - (pLabel + 7);

        memcpy(Num, pLabel + 7, l);
        Num[l] = '\0';
        DataSize = strtol(Num, &pEnd, 10);
        if (*pEnd)
          DataSize = -1;
      }
    }

    Disassemble(Address, &Info, IsData, DataSize);
    if (Info.pRemark)
    {
      PrTabs(pData->pDestFile, pData->MaxLabelLen, 0);
      fprintf(pData->pDestFile, "; %s\n", Info.pRemark);
    }

    if (pLabel)
    {
      fprintf(pData->pDestFile, "%s:", pLabel);
      PrTabs(pData->pDestFile, pData->MaxLabelLen, tabbedstrlen(pLabel) + 1);
    }
    else
      PrTabs(pData->pDestFile, pData->MaxLabelLen, 0);
    fprintf(pData->pDestFile, "%s", Info.SrcLine);

    PrTabs(pData->pDestFile, pData->MaxSrcLineLen, tabbedstrlen(Info.SrcLine));
    fprintf(pData->pDestFile, ";");
    RetrieveCodeFromChunkList(&CodeChunks, Address, Code, Info.CodeLen);
    for (z = 0; z < Info.CodeLen; z++)
    {
      HexString(NumString, sizeof(NumString),  Code[z], 2);
      fprintf(pData->pDestFile, " %s", NumString);
    }
    fputc('\n', pData->pDestFile);

    Address += Info.CodeLen;
  }
}

int main(int argc, char **argv)
{
  LargeWord Address, NextAddress;
  Boolean NextAddressValid;
  tDisassInfo Info;
  unsigned z;
  tDisasmData Data;
  int ThisSrcLineLen;
  as_cmd_results_t cmd_results;

  strutil_init();
  nls_init();
  NLS_Initialize(&argc, argv);
  dasmdef_init();
  cpulist_init();
  msg_level_init();
#ifdef _USE_MSH
  nlmessages_init_buffer(das_msh_data, sizeof(das_msh_data), MsgId1, MsgId2);
#else
  nlmessages_init_file("das.msg", *argv, MsgId1, MsgId2);
#endif
  deco68_init();
  deco87c800_init();
  deco4004_init();
  write_version_exit = write_help_exit = write_cpu_list_exit = False;

  as_cmd_register(DASParams, as_array_size(DASParams));
  if (e_cmd_err == as_cmd_process(argc, argv, pEnvName, &cmd_results))
  {
    fprintf(stderr, "%s%s\n", getmessage(cmd_results.error_arg_in_env ? Num_ErrMsgInvEnvParam : Num_ErrMsgInvParam), cmd_results.error_arg);
    exit(4);
  }

  if ((msg_level >= e_msg_level_verbose) || write_version_exit)
  {
    String Ver;

    as_snprintf(Ver, sizeof(Ver), "DAS V%s", Version);
    WrCopyRight(Ver);
  }

  if (write_help_exit)
  {
    char *ph1, *ph2;
    String Tmp;
    as_snprintf(Tmp, sizeof(Tmp), "%s%s%s", getmessage(Num_InfoMessHead1), as_cmdarg_get_executable_name(), getmessage(Num_InfoMessHead2));
    write_console_next(Tmp);
    for (ph1 = getmessage(Num_InfoMessHelp), ph2 = strchr(ph1, '\n'); ph2; ph1 = ph2 + 1, ph2 = strchr(ph1, '\n'))
    {
      *ph2 = '\0';
      write_console_next(ph1);
      *ph2 = '\n';
    }
  }

  if (write_cpu_list_exit)
  {
    printf("%s\n", getmessage(Num_InfoMessCPUList));
    PrintCPUList(write_console_next);
  }

  if (write_version_exit || write_help_exit || write_cpu_list_exit)
    exit(0);

  if (!Disassemble)
  {
    fprintf(stderr, "no CPU set, aborting\n");
    exit(3);
  }

  /* walk through code */

  NextAddress = 0;
  NextAddressValid = False;
  Data.MaxSrcLineLen = 0;
  while (EntryAddressAvail())
  {
    Address = GetEntryAddress(NextAddressValid, NextAddress);
    Disassemble(Address, &Info, False, -1);
    AddChunk(&UsedCodeChunks, Address, Info.CodeLen, True);
    if ((ThisSrcLineLen = tabbedstrlen(Info.SrcLine)) > Data.MaxSrcLineLen)
      Data.MaxSrcLineLen = ThisSrcLineLen;
    for (z = 0; z < Info.NextAddressCount; z++)
      if (!AddressInChunk(&UsedCodeChunks, Info.NextAddresses[z]))
        AddEntryAddress(Info.NextAddresses[z]);
    NextAddress = Address + Info.CodeLen;
    NextAddressValid = True;
  }

  /* round up src line & symbol length to next multiple of tabs */

  Data.MaxSrcLineLen += TABSIZE - (Data.MaxSrcLineLen % TABSIZE);
  Data.MaxLabelLen = GetMaxInvSymbolNameLen() + 1;
  Data.MaxLabelLen += TABSIZE - (Data.MaxLabelLen % TABSIZE);
  Data.pDestFile = stdout;

  /* bring areas into order */

  SortChunks(&UsedCodeChunks);
  SortChunks(&UsedDataChunks);

  /* dump them out */

  IterateChunks(DisasmIterator, &Data);

  /* summary */

  DumpChunks(&UsedCodeChunks, Data.pDestFile);

  return 0;
}
