/* 
 * ASCD : copyright (c) Aley Keprt 2012
 *
 * samsnap.cpp - Sam Coupé snapshots
 *
    This library is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
 */


#include <stdio.h>

#include "zlib.h"
#include "ZipReader.h"
#include "fdi.h"
#include "version.h"
#include "samsnap.h"
#include "util.h"
#include "samio.h"
#include "cfg.h"
#include "memory.h"
#include "z80cpu/z80.h"
#include "samio.h"
#include "sammouse.h"
#include "air.h"


//Block ID
enum BlockID {
  ID_Error = -1,
  //00-31 reserved for general use
  ID_EOF = 0,
  ID_CPU,
  ID_Ports,
  ID_Memory,
  ID_ExternalMemA,
  ID_ExternalMemB,
  ID_ExternalMemC,
  ID_ExternalMemD,
  ID_Floppy1,
  ID_Floppy2,
  ID_Mouse,
  //32-63 resered for ASCD 
  ID_ASCD_Timings = 32,
  ID_ASCD_OpenAir,
  //64-95 reserved for SimCoupe
  //96-254 reserved for future use
};


#pragma pack(push,1)
struct MouseData {
  WORD ActiveCounter;
  BYTE ByteIndex;
};
#pragma pack(pop)


static const char FILE_ID[] = "SamSnap!"; //file id

static const char EMUL_ID[] = "ASCD"; //emulator id


//---------------------------------------------------------------------------------------------------------------------

//Read block ID and length
//return: block ID, 0 = EOF, -1 = error
static BlockID ReadBlockID(ZipReader &f, int &size) {

  int id = f.getc();
  if(id<0) return ID_Error;
  if(!id) return ID_EOF;

  //read the block size (little endian)
  if(f.read(&size, 4) != 4) return ID_Error;

#ifdef DEBUG_SNAPS
  logmsg(LMINFO, "SCS read block ID=%i", id & 63);
#endif
  return (BlockID)id;
}

//---------------------------------------------------------------------------------------------------------------------

static bool ReadBlockData(ZipReader &f, void *dest, int wanted, int &ondisk) {
#ifdef DEBUG_SNAPS
  logmsg(LMINFO, "Read block data wanted %i, on disk%i", wanted, ondisk);
#endif
  int toread = wanted;
  if(wanted > ondisk) toread = ondisk;
  if(f.read(dest, toread) != toread) return true; //error
  if(wanted > ondisk) memset((BYTE*)dest + toread, 0, wanted - ondisk);
  ondisk -= toread;
  return 0; //ok
}

//---------------------------------------------------------------------------------------------------------------------

//Write a single byte to file
//return: error flag (true=error)
static inline bool WriteByte(gzFile f, int value) {
  return gzputc(f, value) != value;
}


//Write block ID and length
//return: error flag (true=error)
static bool WriteBlockID(gzFile f, BlockID id, int size) {
  if(id < 1 || id > 255) return true; //invalid id
#ifdef DEBUG_SNAPS
  logmsg(LMINFO, "SCS Write block head ID=%i size=%i", id, size);
#endif
  if(WriteByte(f, id)) return true; //error
  return gzwrite(f, &size, 4) != 4;
}


//Write whole block from memory buffer
//return: error flag (true=error)
static bool WriteBlock(gzFile f, BlockID id, void *data, int size) {
  if(WriteBlockID(f, id, size)) return true;
#ifdef DEBUG_SNAPS
  logmsg(LMINFO, "SCS Write block data ID=%i size=%i", id, size);
#endif
  if(size==0) return false; //ok :-)
  return gzwrite(f, data, size) != size;
}


//Read block's data
//input: wanted=wanted size, ondisk=size on disk
//return: error flag (true=error)
//remarks: The ondisk value is decremented by the size of data actually read.
//If wanted > read, then the rest of dest buffer is filled with zeros and ondisk = 0
//If wanted < read, then ondisk -= wanted

//---------------------------------------------------------------------------------------------------------------------

//Save a SamSnap file
//return: error flag (true=error)
bool ScsSave(const char *fname) {

  gzFile gzf = gzopen(fname, "wb");
  if(!gzf) {
    logmsg(LMERROR, "Failed to create snap file %s", fname);
    return 1; //error
  }

  //file id
  gzwrite(gzf, FILE_ID, sizeof(FILE_ID)-1);

  //emulator id and version
  gzwrite(gzf, EMUL_ID, sizeof(EMUL_ID)-1);
  WriteByte(gzf, VERSION_MAJOR);
  WriteByte(gzf, VERSION_MINOR);
  
  //hardware configuration byte
  //0=Sam, 1=ZXS, 2=ZXS128
  WriteByte(gzf, cfg.zxmode);

  switch(cfg.zxmode) {
  
  case EM_Sam:  
    //256/512KB memory
    WriteBlock(gzf, ID_Memory, MemoryGetPage(0), (1024*256) << GetCfg(sam512));

    //external memory
    //in emulator we use banks in order DCBA (backwards) - this is needed for MasterDOS
    //in file we save blocks in ABCD order (forwards)
    for(int i=0; i<4; i++) {
      if(4 - GetCfg(extmem) <= i) WriteBlock(gzf, (BlockID)(ID_ExternalMemA + i), MemoryGetExtendedRamPtr(i), 1024*1024);
    }
    break;

  case EM_128k:
    WriteBlock(gzf, ID_Memory, MemoryGetPage(0), 1024*128);
    break;

  case EM_48k:
    BYTE buf[49152];
    memcpy(buf, theRdMemory[1], 16384);
    memcpy(buf+16384, theRdMemory[2], 16384);
    memcpy(buf+32768, theRdMemory[3], 16384);
    WriteBlock(gzf, ID_Memory, buf, 49152);
    break;
  }

  //CPU registers - little endian format
  //BYTE a, f, b, c, d, e, h, l;
  //BYTE a1, f1, b1, c1, d1, e1, h1, l1;
  //WORD ix, iy, pc, sp;
  //BYTE i, r, flags, __reserved__;
  //WORD LineTs, IntActiveCounter, MouseActiveCounter;
  Z80_Regs regs;
  Z80GetRegs(&regs);
  //here reorder the bytes of ix, iy, pc, sp on big-endian computer!!!
  //don't store last 2 bytes (mouse strobe time)
  WriteBlock(gzf, ID_CPU, &regs, sizeof(regs)-2);

  if(cfg.zxmode == EM_Sam) {
    //Sam Coupe ports
    //BYTE sampal[16];
    //BYTE saaregs[29];
    //BYTE saareg;
    //BYTE vmpr,hmpr,lmpr,border,line_int,hepr,lepr,lpen,attr,status_reg;
    SamSnapPorts ports;
    getsamports(&ports);
    WriteBlock(gzf, ID_Ports, &ports, sizeof(ports));

    //Write floppy disk blocks
    for(int i=0; i<2; i++) {
      VL1772_Regs regs;
      FdiGetRegs(i, &regs);
      WriteBlock(gzf, (BlockID)(ID_Floppy1 + i), &regs, sizeof(regs));
    }

    //Sam Mouse block
    //ActiveCounter: number of t-states elapsed since mouse strobe
    //ByteIndex: index of next data byte to be retrieved from mouse port
    //index 0 and 1 is for keyboard state, index 2 is mouse buttons state, index 3-8 is mouse position
    //mouse is strobed when this value is nonzero
    if(cfg.mouse) {
      MouseData data;
      data.ByteIndex = 0;
      data.ActiveCounter = regs.MouseActiveCounter;
      if(MouseStrobed) data.ByteIndex = MouseGetByteIndex() + 2;
      WriteBlock(gzf, ID_Mouse, &data, 3);
    }
  } else {
    //ZXS ports
    //we always save all ports including AY, even in 48K mode
    ZXSSnapPorts ports;
    getzxsports(&ports);
    WriteBlock(gzf, ID_Ports, &ports, sizeof(ports));
  }

  //ASCD - OpenAir state
  if(Air_getmode()==AM_record) {
    AirState state;
    Air_GetState(&state);
    logmsg(LMINFO, "Saving AIR state: filepos=%i, elapsed_frames=%i", state.filepos, state.elapsed_frames);
    WriteBlock(gzf, ID_ASCD_OpenAir, &state, sizeof(state));
  }

  //ASCD timings
  //notimpl

  //EOF marker
  WriteByte(gzf, 0);

  bool result = gzclose(gzf)!=0;
  gzf = 0;
  if(result) logmsg(LMERROR, "Error writing snap file %s", fname);
  return result;
}

//---------------------------------------------------------------------------------------------------------------------

//Read a SamSnap file
//return: 0=OK, else error
bool ScsLoad(ZipReader &f) {

  char idtext[sizeof(FILE_ID)];
  idtext[0] = 0;
  f.read(idtext, sizeof(FILE_ID)-1);
  if(memcmp(idtext, "SamSnap!", sizeof(FILE_ID)-1)) {
    //bad header
    logmsg(LMERROR, "File %s is in an invalid format", f.getname());
    return true; //error
  }

  //skip emulator id and version info (6 bytes)
  f.seek(6, SEEK_CUR);

  //hardware configuration byte
  int zxmode = f.getc();
  if(zxmode < 0 || zxmode > 2) {
    logmsg(LMERROR, "Unsupported hardware mode %i", zxmode);
    return true; //error
  }
  set_emulation_mode((EM)zxmode);

  //read data blocks
  BlockID id = ID_EOF;
  int extmem = 0;
  while(true) {
    int size = 0;
    id = ReadBlockID(f, size);
    if(id==ID_EOF || id==ID_Error) break;

    #define ERROR { logmsg(LMERROR, "Error in snapshot file: ID=%i, size=%i", id, size); return true; }

    switch(id) {

    case ID_CPU:
      Z80_Regs regs;
      if(ReadBlockData(f, &regs, sizeof(regs), size)) ERROR;
      Z80SetRegs(&regs);

      //PAL line number is stored in CPU data, but is used elsewhere, so we need to update our counter manually
      extern int LineNo;
      LineNo = regs.lineno;

      //bit 2 in iff means that instrrupts should be processed before next instruction
      extern bool do_interrupts_before_instruction;
      if(regs.iff & 4) do_interrupts_before_instruction = true;
      break;

    case ID_Memory:
      //48/128/256/512KB memory
      //this is always the first block in the file
      switch(cfg.zxmode) {
      case EM_Sam:
        if(ReadBlockData(f, MemoryGetPage(0), (1024*256) << GetCfg(sam512), size)) ERROR;
        break;
      case EM_128k:
        if(ReadBlockData(f, MemoryGetPage(0), 1024*128, size)) ERROR;
        break;
      case EM_48k:
        if(ReadBlockData(f, MemoryGetPage(5), 16384, size)) ERROR;
        if(ReadBlockData(f, MemoryGetPage(2), 16384, size)) ERROR;
        if(ReadBlockData(f, MemoryGetPage(0), 16384, size)) ERROR;
        break;
      }
      break;

    case ID_Ports:
      if(cfg.zxmode==EM_Sam) {
        //Sam ports
        SamSnapPorts ports;
        if(ReadBlockData(f, &ports, sizeof(ports), size)) ERROR;
        ports.status_reg |= 0xf6; //nepouzivane bity potrebujeme drzet na 1
        setsamports(&ports);
      } else {
        //ZXS ports
        ZXSSnapPorts ports;
        if(ReadBlockData(f, &ports, sizeof(ports), size)) ERROR;
        setzxsports(&ports);
      }
      break;

    case ID_ExternalMemA:
    case ID_ExternalMemB:
    case ID_ExternalMemC:
    case ID_ExternalMemD:
      if(extmem < ID_ExternalMemA + 4 - id) extmem = ID_ExternalMemA + 4 - id;
      ReadBlockData(f, MemoryGetExtendedRamPtr(id - ID_ExternalMemA), 1024*1024, size);
      break;

    case ID_Floppy1:
    case ID_Floppy2:
      VL1772_Regs fdiregs;
      if(ReadBlockData(f, &fdiregs, sizeof(fdiregs), size)) ERROR;
      FdiSetRegs(id - ID_Floppy1, &fdiregs);
      break;

    case ID_Mouse:
      MouseData data;
      if(ReadBlockData(f, &data, sizeof(data), size)) ERROR;
      SetCfg(mouse,1);
      regs.MouseActiveCounter = data.ActiveCounter;
      MouseStrobed = 0;
      if(data.ByteIndex) {
        MouseStrobed = 1;
        MouseSetByteIndex(((int)data.ByteIndex) - 2); //signed -2
      }
      break;

    case ID_ASCD_Timings:
      break; //notimpl

    case ID_ASCD_OpenAir:
      if(Air_getmode()==AM_record) {
        AirState state;
        if(ReadBlockData(f, &state, sizeof(state), size)) ERROR;
        Air_SetState(&state);
      }
      break;
    }

    //skip unneeded data
    if(size>0 && f.seek(size, SEEK_CUR)<0) break;
  }
#undef ERROR

  cfg.extmem = extmem;

  return false; //ok
}
