/*****************************************************************************/
/* D2K: A Doom Source Port for the 21st Century                              */
/*                                                                           */
/* Copyright (C) 2014: See COPYRIGHT file                                    */
/*                                                                           */
/* This file is part of D2K.                                                 */
/*                                                                           */
/* D2K 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.                                                            */
/*                                                                           */
/* D2K 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 General Public License for more    */
/* details.                                                                  */
/*                                                                           */
/* You should have received a copy of the GNU General Public License along   */
/* with D2K.  If not, see <http://www.gnu.org/licenses/>.                    */
/*                                                                           */
/*****************************************************************************/


#include "z_zone.h"

#include "m_argv.h"
#include "m_misc.h"
#include "d_event.h"
#include "d_res.h"
#include "e6y.h"
#include "g_comp.h"
#include "g_overflow.h"
#include "p_defs.h"
#include "p_map.h"
#include "p_maputl.h"
#include "pl_main.h"
#include "r_defs.h"
#include "r_state.h"
#include "w_wad.h"

overrun_param_t overflows[OVERFLOW_MAX];

const char *overflow_cfgname[OVERFLOW_MAX] = {
  "overrun_spechit_emulate",
  "overrun_reject_emulate",
  "overrun_intercept_emulate",
  "overrun_playeringame_emulate",
  "overrun_donut_emulate",
  "overrun_missedbackside_emulate"
};

static void ShowOverflowWarning(overrun_list_t overflow,
                                int fatal,
                                const char *params, ...) {
  overflows[overflow].shit_happens = true;

  if (overflows[overflow].warn && !overflows[overflow].promted) {
    va_list argptr;
    char buffer[1024];

    static const char *name[OVERFLOW_MAX] = {
      "SPECHIT", "REJECT", "INTERCEPT", "PLYERINGAME", "DONUT",
      "MISSEDBACKSIDE"
    };

    static const char str1[] =
      "Too big or not supported %s overflow has been detected. "
      "Desync or crash can occur soon "
      "or during playback with the vanilla engine in case you're recording "
      "demo.%s%s";
    
    static const char str2[] = 
      "%s overflow has been detected.%s%s";

    static const char str3[] = 
      "%s overflow has been detected. "
      "The option responsible for emulation of this overflow is switched off "
      "hence desync or crash can occur soon "
      "or during playback with the vanilla engine in case you're recording "
      "demo.%s%s";

    overflows[overflow].promted = true;

    sprintf(buffer,
      (fatal ? str1 : (EMULATE(overflow) ? str2 : str3)), 
      name[overflow],
      "\nYou can change " PACKAGE_TARNAME " behaviour for this overflow "
      "through in-game menu.",
      params);
    
    va_start(argptr, params);
    D_VMsgLocalWarn(buffer, argptr);
    va_end(argptr);
  }
}

// e6y
//
// Intercepts Overrun emulation
// See more information on:
// doomworld.com/vb/doom-speed-demos/35214-spechits-reject-and-intercepts-overflow-lists
//
// Thanks to Simon Howard (fraggle) for refactor the intercepts
// overrun code so that it should work properly on big endian machines
// as well as little endian machines.

// Overwrite a specific memory location with a value.
static void InterceptsMemoryOverrun(int location, int value) {
  int i = 0;
  int offset = 0;
  int index;
  void *addr;

  // Search down the array until we find the right entry

  while (intercepts_overrun[i].len != 0) {
    if (offset + intercepts_overrun[i].len > location) {
      addr = intercepts_overrun[i].addr;

      // Write the value to the memory location.
      // 16-bit and 32-bit values are written differently.

      if (addr != NULL) {
        if (intercepts_overrun[i].int16_array) {
          index = (location - offset) / 2;
          ((short *) addr)[index] = value & 0xffff;
          ((short *) addr)[index + 1] = (value >> 16) & 0xffff;
        }
        else {
          index = (location - offset) / 4;
          ((int *) addr)[index] = value;
        }
      }

      break;
    }

    offset += intercepts_overrun[i].len;
    ++i;
  }
}

void InterceptsOverrun(int num_intercepts, intercept_t *intercept) {
  if (num_intercepts > MAXINTERCEPTS_ORIGINAL &&
      demo_compatibility &&
      PROCESS(OVERFLOW_INTERCEPT)) {
    ShowOverflowWarning(OVERFLOW_INTERCEPT, false, "");

    if (EMULATE(OVERFLOW_INTERCEPT)) {
      int location = (num_intercepts - MAXINTERCEPTS_ORIGINAL - 1) * 12;

      // Overwrite memory that is overwritten in Vanilla Doom, using
      // the values from the intercept structure.
      //
      // Note: the ->d.{thing,line} member should really have its
      // address translated into the correct address value for 
      // Vanilla Doom.

      InterceptsMemoryOverrun(location, intercept->frac);
      InterceptsMemoryOverrun(location + 4, intercept->isaline);
      InterceptsMemoryOverrun(location + 8, (int) intercept->d.thing);
    }
  }
}

// e6y
// playeringame overrun emulation
// it detects and emulates overflows on vex6d.wad\bug_wald(toke).lmp, etc.
// http://www.doom2.net/doom2/research/runningbody.zip
// [CG] [FIXME] Doesn't work after the players refactor (probably)

int PlayeringameOverrun(const mapthing_t *mthing) {
  if (mthing->type == 0 && PROCESS(OVERFLOW_PLYERINGAME)) {
    player_t *player = P_PlayersLookup(4);
    bool didsecret = false;

    if (player) {
      didsecret = player->didsecret;
    }

    // playeringame[-1] == players[3].didsecret
    ShowOverflowWarning(OVERFLOW_PLYERINGAME, didsecret, "");

    if (EMULATE(OVERFLOW_PLYERINGAME)) {
      return true;
    }
  }

  return false;
}

//
// spechit overrun emulation
//

unsigned int spechit_baseaddr = 0;

// e6y
// Code to emulate the behavior of Vanilla Doom when encountering an overrun
// of the spechit array.
// No more desyncs on compet-n\hr.wad\hr18*.lmp, all strain.wad\map07 demos
// etc.
// http://www.doomworld.com/vb/showthread.php?s=&threadid=35214
// See more information on:
// doomworld.com/vb/doom-speed-demos/35214-spechits-reject-and-intercepts-overflow-lists
void SpechitOverrun(spechit_overrun_param_t *params) {
  int numspechit = *(params->numspechit);

  if (demo_compatibility && numspechit > 8) {
    line_t **spechit = *(params->spechit);

    ShowOverflowWarning(OVERFLOW_SPECHIT,
      numspechit > 
        (compatibility_level == dosdoom_compatibility || 
        compatibility_level == tasdoom_compatibility ? 10 : 14), 
      "\n\nThe list of LineID leading to overrun:\n%d, %d, %d, %d, %d, %d, "
      "%d, %d, %d.",
      spechit[0]->iLineID, spechit[1]->iLineID, spechit[2]->iLineID,
      spechit[3]->iLineID, spechit[4]->iLineID, spechit[5]->iLineID,
      spechit[6]->iLineID, spechit[7]->iLineID, spechit[8]->iLineID);

    if (EMULATE(OVERFLOW_SPECHIT)) {
      unsigned int addr;

      if (spechit_baseaddr == 0) {
        int p;

        // This is the first time we have had an overrun.  Work out
        // what base address we are going to use.
        // Allow a spechit value to be specified on the command line.

        //
        // Use the specified magic value when emulating spechit overruns.
        //

        p = M_CheckParm("-spechit");
        
        if (p > 0) {
          //baseaddr = atoi(myargv[p+1]);
          M_StrToInt(myargv[p+1], (int*)&spechit_baseaddr);
        }
        else {
          spechit_baseaddr = DEFAULT_SPECHIT_MAGIC;
        }
      }

      // Calculate address used in doom2.exe

      addr = spechit_baseaddr + (params->line - lines) * 0x3E;

      if (compatibility_level == dosdoom_compatibility ||
          compatibility_level == tasdoom_compatibility) {
        // There are no more desyncs in the following dosdoom demos: 
        // flsofdth.wad\fod3uv.lmp - http://www.doomworld.com/sda/flsofdth.htm
        // hr.wad\hf181430.lmp - http://www.doomworld.com/tas/hf181430.zip
        // hr.wad\hr181329.lmp - http://www.doomworld.com/tas/hr181329.zip
        // icarus.wad\ic09uv.lmp - http://competn.doom2.net/pub/sda/i-o/icuvlmps.zip

        switch(numspechit) {
        case 9: 
          *(params->tmfloorz) = addr;
          break;
        case 10:
          *(params->tmceilingz) = addr;
          break;
        default:
          fprintf(stderr,
            "SpechitOverrun: Warning: unable to emulatean overrun where "
            "numspechit=%i\n",
            numspechit
          );
          break;
        }
      }
      else {
        switch(numspechit) {
        case 9: 
        case 10:
        case 11:
        case 12:
          params->tmbbox[numspechit-9] = addr;
          break;
        case 13:
          *(params->nofit) = addr;
          break;
        case 14:
          *(params->crushchange) = addr;
          break;
        default:
          D_MsgLocalError(
            "SpechitOverrun: Warning: unable to emulate an overrun where "
            "numspechit=%i\n",
            numspechit
          );
          break;
        }
      }
    }
  }
}

//
// reject overrun emulation
//

// padding the reject table if it is too short
// totallines must be the number returned by P_GroupLines()
// an underflow will be padded with zeroes, or a doom.exe z_zone header
// 
// e6y
// reject overrun emulation code
// It's emulated successfully if the size of overflow no more than 16 bytes.
// No more desync on teeth-32.wad\teeth-32.lmp.
// http://www.doomworld.com/vb/showthread.php?s=&threadid=35214

void RejectOverrun(int rejectlump, const unsigned char **rejectmatrix,
                                   int totallines) {
  unsigned int length = W_LumpLength(rejectlump);
  unsigned int required = (numsectors * numsectors + 7) / 8;
  unsigned char *newreject;
  unsigned char pad;


  if (length < required) {
    // allocate a new block and copy the reject table into it; zero the rest
    // PU_LEVEL => will be freed on level exit
    newreject = Z_Malloc(required, PU_LEVEL, NULL);
    *rejectmatrix = memmove(newreject, *rejectmatrix, length);

    // e6y
    // PrBoom 2.2.5 and 2.2.6 padded a short REJECT with 0xff
    // This command line switch is needed for all potential demos 
    // recorded with these versions of PrBoom on maps with too short REJECT
    // I don't think there are any demos that will need it but yes that seems
    // sensible
    pad = prboom_comp[PC_REJECT_PAD_WITH_FF].state ? 0xff : 0;

    memset(newreject + length, pad, required - length);
    // unlock the original lump, it is no longer needed
    W_UnlockLumpNum(rejectlump);
    rejectlump = -1;

    if (demo_compatibility && PROCESS(OVERFLOW_REJECT)) {
      ShowOverflowWarning(
        OVERFLOW_REJECT,
        (required - length > 16) || (length % 4 != 0),
        ""
      );

      if (EMULATE(OVERFLOW_REJECT)) {
        // merged in RejectOverrunAddInt(), and the 4 calls to it, here
        unsigned int rejectpad[4] = {
          0,        // size, will be filled in using totallines
          0,        // part of the header of a doom.exe z_zone block
          50,       // DOOM_CONST_PU_LEVEL
          0x1d4a11  // DOOM_CONST_ZONEID
        };
        unsigned int i;
        unsigned int pad = 0;
        unsigned int *src = rejectpad;
        unsigned char *dest = newreject + length;

        // doom.exe zone header size
        rejectpad[0] = ((totallines * 4 + 3) & ~3) + 24;

        // copy at most 16 bytes from rejectpad
        // emulating a 32-bit, little-endian architecture (can't memmove)

        // 16 hard-coded
        for (i = 0; i < (unsigned int)(required - length) && i < 16; i++) {
          if (!(i & 3)) { // get the next 4 bytes to copy when i=0,4,8,12
            pad = *src++;
          }
          *dest++ = pad & 0xff; // store lowest-significant byte
          pad >>= 8; // rotate the next byte down
        }
      }
    }

    D_MsgLocalWarn("P_LoadReject: REJECT too short (%u<%u) - padded\n",
      length,
      required
    );
  }
}

//
// Read Access Violation emulation.
//

// C:\>debug
// -d 0:0
//
// DOS 6.22:
// 0000:0000  (57 92 19 00) F4 06 70 00-(16 00)
// DOS 7.1:
// 0000:0000  (9E 0F C9 00) 65 04 70 00-(16 00)
// Win98:
// 0000:0000  (9E 0F C9 00) 65 04 70 00-(16 00)
// DOSBox under XP:
// 0000:0000  (00 00 00 F1) ?? ?? ?? 00-(07 00)

#define DOS_MEM_DUMP_SIZE 10

unsigned char mem_dump_dos622[DOS_MEM_DUMP_SIZE] = {
  0x57, 0x92, 0x19, 0x00, 0xF4, 0x06, 0x70, 0x00, 0x16, 0x00
};

unsigned char mem_dump_win98[DOS_MEM_DUMP_SIZE] = {
  0x9E, 0x0F, 0xC9, 0x00, 0x65, 0x04, 0x70, 0x00, 0x16, 0x00
};

unsigned char mem_dump_dosbox[DOS_MEM_DUMP_SIZE] = {
  0x00, 0x00, 0x00, 0xF1, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00
};

unsigned char *dos_mem_dump = mem_dump_dos622;

static int GetMemoryValue(unsigned int offset, void *value, int size) {
  static int firsttime = true;

  if (firsttime) {
    int p;
    int i;
    int val;

    firsttime = false;
    i = 0;

    if ((p = M_CheckParm("-setmem")) && (p < myargc - 1)) {
      if (!strcasecmp(myargv[p + 1], "dos622")) {
        dos_mem_dump = mem_dump_dos622;
      }
      if (!strcasecmp(myargv[p + 1], "dos71")) {
        dos_mem_dump = mem_dump_win98;
      }
      else if (!strcasecmp(myargv[p + 1], "dosbox")) {
        dos_mem_dump = mem_dump_dosbox;
      }
      else {
        while (++p != myargc && *myargv[p] != '-' && i < DOS_MEM_DUMP_SIZE) {
          M_StrToInt(myargv[p], &val);
          dos_mem_dump[i++] = (unsigned char)val;
        }
      }
    }
  }

  if (value) {
    switch (size)
    {
    case 1:
      *((unsigned char*)value) = *((unsigned char*)(&dos_mem_dump[offset]));
      return true;
    case 2:
      *((unsigned short*)value) = *((unsigned short*)(&dos_mem_dump[offset]));
      return true;
    case 4:
      *((unsigned int*)value) = *((unsigned int*)(&dos_mem_dump[offset]));
      return true;
    }
  }

  return false;
}

//
// donut overrun emulation (linedef action #9)
//

#define DONUT_FLOORPIC_DEFAULT 0x16
int DonutOverrun(fixed_t *pfloorheight, short *pfloorpic) {
  if (demo_compatibility && PROCESS(OVERFLOW_DONUT)) {
    ShowOverflowWarning(OVERFLOW_DONUT, 0, "");

    if (EMULATE(OVERFLOW_DONUT)) {
      if (pfloorheight && pfloorpic) {
        GetMemoryValue(0, pfloorheight, 4);
        GetMemoryValue(8, pfloorpic, 2);
        
        // bounds-check floorpic
        if ((*pfloorpic) <= 0 || (*pfloorpic) >= numflats) {
          *pfloorpic = MIN(numflats - 1, DONUT_FLOORPIC_DEFAULT);
        }

        return true;
      }
    }
  }

  return false;
}


int MissedBackSideOverrun(line_t *line) {
  if (demo_compatibility) {
    if (line) {
      ShowOverflowWarning(OVERFLOW_MISSEDBACKSIDE, 0,
        "\n\nLinedef %d has two-sided flag set, but no second sidedef",
        line->iLineID
      );
    }
    else {
      ShowOverflowWarning(OVERFLOW_MISSEDBACKSIDE, 0, "");
    }
  }

  return false;
}

//
// GetSectorAtNullAddress
//
sector_t* GetSectorAtNullAddress(void) {
  static int null_sector_is_initialized = false;
  static sector_t null_sector;

  if (demo_compatibility && EMULATE(OVERFLOW_MISSEDBACKSIDE)) {
    if (!null_sector_is_initialized) {
      memset(&null_sector, 0, sizeof(null_sector));
      null_sector.flags = NULL_SECTOR;
      GetMemoryValue(0, &null_sector.floorheight, 4);
      GetMemoryValue(4, &null_sector.ceilingheight, 4);
      null_sector_is_initialized = true;
    }

    return &null_sector;
  }

  return 0;
}

/* vi: set et ts=2 sw=2: */
