/*
Jupiter ACE Forth Interpreter and Compiler

Copyright 2025 Pedro Gimeno Fortea

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/

#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>

#include "z80.h"

#define sv_PRPOSN     0x3C1C   // actually called SCRPOS in the manual
#define sv_STARTBUF   0x3C1E   // actually called INSCRN in the manual
#define sv_CURSOR     0x3C20
#define sv_ENDBUF     0x3C22
#define sv_L_HALF     0x3C24
#define sv_ERR_NO     0x3C3D

// Maximum buffer size supported by the screen buffer, 22*32-2 (22 lines
// minus 2 characters)
#define IBUFSZ 702

static z80 cpu[1];
static uint8_t memory[65536];

#define BITS64 0
#if BITS64
  static uint64_t attention[1024];
  #define PAY_ATTENTION_TO(a) (attention[(a) >> 6] |= (uint64_t)1 << ((a) & 63))
  #define NEEDS_ATTENTION(a) (attention[(a) >> 6] & (uint64_t)1 << ((a) & 63))
#else
  static uint32_t attention[2048];
  #define PAY_ATTENTION_TO(a) (attention[(a) >> 5] |= (uint32_t)1 << ((a) & 31))
  #define NEEDS_ATTENTION(a) (attention[(a) >> 5] & (uint32_t)1 << ((a) & 31))
#endif

static const char *tapeName;

static void writemem_callback(void* userdata, uint16_t addr, uint8_t val) {
  if (userdata) { }
  if (addr >= 0x2000) // no writes to ROM
  {
    // spend more time on writes when processing mirrors, so that reads are
    // faster
    if ((addr | 0x7FF) == 0x27FF)  // bgram
    {
      // two mirrors each
      memory[addr & ~0x0400] = val;
      memory[addr |  0x0400] = val;
    }
    else if ((addr | 0x0FFF) == 0x3FFF)
    {
      // four mirrors
      addr &= ~0x0C00;
      memory[addr | 0x0000] = val;
      memory[addr | 0x0400] = val;
      memory[addr | 0x0800] = val;
      memory[addr | 0x0C00] = val;
    }
    else
      memory[addr] = val;
  }
}

static uint8_t readmem_callback(void* userdata, uint16_t addr)
{
  if (userdata) { }
  return memory[addr];
}

static uint8_t in_callback(z80* const z, uint16_t port)
{
  return 255;
}

static void out_callback(z80* const z, uint16_t port, uint8_t val)
{
  // what did you expect from a headless emulator?
}

static inline uint8_t peek(uint16_t addr)
{
  return readmem_callback(0, addr);
}

static inline void poke(uint16_t addr, uint8_t value)
{
  writemem_callback(0, addr, value);
}

static inline void wpoke(uint16_t addr, uint16_t value)
{
  poke(addr, value & 0xFF);
  poke(addr + 1, value >> 8);
}

static inline uint16_t wpeek(uint16_t addr)
{
  return peek(addr) | (uint16_t)peek(addr + 1) << 8;
}

// Utility to dump a portion of memory. Unused. Not static to avoid warning.
/*static*/ void dump(uint16_t start, uint16_t len)
{
  do
  {
    printf("\n%04X ", start);
    for (int i = 0; i < 16; ++i)
    {
      printf(i == 8 ? "-%02X" : " %02X", peek(start++));
      if (!--len) break;
    }
  }
  while (len);
  puts("");
}

static int showline()
{
  // The input buffer starts with a NUL, and during REQUERY, right
  // after it comes the [?] cursor. So we need to skip those two
  // positions, the NUL and the [?].
  fwrite(memory + wpeek(sv_STARTBUF) + 2,
    1, wpeek(sv_ENDBUF) - wpeek(sv_STARTBUF) - 2, stderr);
  fputs("\n", stderr);
  return 1;
}

static int error(const char *msg)
{
  fprintf(stderr, "\nError: %s\n", msg);
  showline();
  return 1;
}

static int error2(const char *msg)
{
  fprintf(stderr, "\n%s\n", msg);
  showline();
  return 1;
}

int main(int argc, const char *argv[])
{
  int linelen;
  bool firstSave = true;
  bool specFormat = false;

  memset(memory, 0, sizeof(memory));
  memset(attention, 0, sizeof(attention));

  if (argc > 1)
    tapeName = argv[1];

  uint8_t linebuf[IBUFSZ + 1];

  {
    FILE *f = fopen("ace.rom", "rb");
    if (!f)
    {
      fputs("Can't open ROM file " "ace.rom" "\n", stderr);
      return 2;
    }
    fread(memory, 1, 8192, f);
    fclose(f);
  }

  z80_init(cpu);
  cpu->read_byte = readmem_callback;
  cpu->write_byte = writemem_callback;
  cpu->port_in = in_callback;
  cpu->port_out = out_callback;

  PAY_ATTENTION_TO(0x0008);  // Trap writes of characters
  PAY_ATTENTION_TO(0x0043);  // Trap to enable INVIS mode at start
  PAY_ATTENTION_TO(0x00AD);  // Trap Forth errors
  PAY_ATTENTION_TO(0x0580);  // Trap RETYPE after moving the cursor
  PAY_ATTENTION_TO(0x059B);  // Trap where QUERY waits for the ENTER key
  PAY_ATTENTION_TO(0x0F40);  // Trap before updating DICT (fix ROM bug)
  PAY_ATTENTION_TO(0x1820);  // Trap tape save
  PAY_ATTENTION_TO(0x18A7);  // Trap tape load, to give an error

  do
  {
    if (NEEDS_ATTENTION(cpu->pc))
    {
      // Attaboy! Deal with special addresses
      switch (cpu->pc)
      {
        case 0x0008:
        {
          // Trap writes of characters
          if (cpu->a == 13)
            puts("");
          else
            fputc(cpu->a, stdout);
          fflush(stdout);
          cpu->pc = 0x017D; // address of a ret
          break;
        }
        case 0x0043:
        {
          // Trap after copying of initial environment to RAM
          poke(0x3C3E, 0x10);  // Set INVIS mode
          break;
        }
        case 0x00AD:
        {
          // Trap ABORT
          char buf[200];
          const char *errstr = buf;
          switch (peek(sv_ERR_NO))
          {
            case 255:
              errstr = 0; break;
            case 1:
              errstr = "Error 1 - Not enough memory";                   break;
            case 2:
              errstr = "Error 2 - Data stack underflow";                break;
            case 3:
              errstr = "Error 3 - BREAK key pressed";                   break;
            case 4:
              errstr = "Error 4 - Compiling word used in interpret mode";
              break;
            case 5:
              errstr = "Error 5 - Syntax error (improper nesting)";     break;
            case 6:
              errstr = "Error 6 - No name or name length > 63";         break;
            case 7:
              errstr = "Error 7 - PICK or ROLL with non-positive index";break;
            case 8:
              errstr = "Error 8 - Floating point overflow";             break;
            case 9:
              errstr = "Error 9 - Trying to print in input buffer";     break;
            case 10:
              errstr = "Error 10 - Tape error";                         break;
            case 11:
              errstr = "Error 11 - Error in REDEFINE or FORGET";        break;
            case 12:
              errstr = "Error 12 - Incomplete definition in dictionary";break;
            case 13:
              errstr = "Error 13 - Word not found, or ROM or \"FORTH\"";break;
            case 14:
              errstr = "Error 14 - Word unlistable";                    break;
            default:
              sprintf(buf, "Unknown error code: %d", peek(sv_ERR_NO));
              return error2(buf);
          }
          if (errstr)
            return error(errstr);
          break;
        }
        case 0x0580:
        {
          // Trap RETYPE - we don't handle it properly nor attempt to, and
          // when invoked from LINE, it means an unknown word has been found.
          // This is not intended to be an interactive interpreter, so we give
          // an error and exit.
          return error("Unknown word");
        }
        case 0x059B:
        {
          // Trap where QUERY waits for the ENTER key
          if (!fgets((char *)linebuf, IBUFSZ + 1, stdin))
            return 0;  // end of file = end of processing
          linelen = strlen((char *)linebuf);
          // Remove trailing LF if present
          if (linelen > 0 && linebuf[linelen - 1] == '\n')
            --linelen;
          // Remove trailing CR if present
          if (linelen > 0 && linebuf[linelen - 1] == '\r')
            --linelen;
          // Set up the buffer to start at 0x2440, 2 lines into the BG RAM.
          // That's the maximum length that inputting through keyboard allows.
          poke(0x2440, 0);
          memset(memory + 0x2441, 32, IBUFSZ);
          wpoke(sv_PRPOSN, 0x2400); // Print position at the start of screen.
          wpoke(sv_STARTBUF, 0x2440); // INSCRN (input buffer begin)
          wpoke(sv_CURSOR, 0x2441 + linelen); // CURSOR position
          wpoke(sv_ENDBUF, 0x2441 + linelen + 1); // can be just out of screen
          wpoke(sv_L_HALF, 0x2440);
          memcpy(memory + 0x2441, linebuf, linelen);
          memory[0x2441 + linelen] = 0x97; // cursor
          //dump(0x2400, 0x400);

          cpu->pc = 0x059F;  // skip the Enter wait loop
          break;
        }
        case 0x0F40:
        {
          // Fix nasty ROM bug that can cause a write to the data stack
          if (cpu->f & 1)     // carry from SBC? (means [DICT] > [STKBOT])
            cpu->pc = 0x0F43; // skip over storing of length word
          break;
        }
        case 0x1820:
        {
          // Trap ROM save
          if (!tapeName)
            return error("(B)SAVE command issued but tape file not provided");
          FILE *f = fopen(tapeName, firstSave ? "wb" : "ab");
          if (!f)
          {
            fprintf(stderr, "Can't open file for writing: %s\n", tapeName);
            return 2;
          }
          fputc((cpu->de + (uint16_t)(specFormat + 1)) & 0xFF, f);
          fputc((cpu->de + (uint16_t)(specFormat + 1)) >> 8, f);
          if (specFormat)
            fputc(cpu->c, f);
          uint8_t xor = 0;
          for (uint16_t i = cpu->hl; i - cpu->hl != cpu->de; ++i)
          {
            xor ^= peek(i);
          }
          // write all at once
          fwrite(memory + cpu->hl, cpu->de, 1, f);
          fputc(xor, f);
          fclose(f);
          firstSave = false;
          cpu->pc = 0x1894;  // Finish save
          break;
        }
        case 0x18A7:
        {
          // Trap ROM load to give an error
          return error("(B)LOAD/(B)VERIFY commands not implemented");
        }
        default: ;
      }
    }
    cpu->halted = 0; // exit from halt state immediately - no rest for you!
    z80_step(cpu);
  }
  while (1);

  return 0;
}

