///////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2015 The 8-Bit Bunch. Licensed under the Apache License, Version 1.1
// (the "License"); you may not use this file except in compliance with the License.
// You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-1.1>.
// Unless required by applicable law or agreed to in writing, software distributed under
// the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
// ANY KIND, either express or implied. See the License for the specific language
// governing permissions and limitations under the License.
///////////////////////////////////////////////////////////////////////////////////////////////////

include "gamelib.plh"
include "globalDefs.plh"
include "playtype.plh"
include "diskops.plh"
include "party.plh"
include "sndseq.plh"
include "import.plh"
include "gen_images.plh"
include "gen_modules.plh"
include "gen_players.plh"

const fontEngine    = $EC00   // main mem LC
const fontEngineLen = $F00    // really a bit less, but this leaves space for debug code
const fontData      = $FB00   // main mem LC
const fontDataLen   = $4FA    // really only $474, but we need to fill all gaps
const soundCode     = $FF80   // main mem LC
const soundLen      = $7A     // FF80.FFF9

const expandVec     = $200    // aux mem (only for raycaster)
const expandMax     = $3600   // max size of unsplit expander

// Exported functions go here. First a predef for each one, then a table with function pointers
// in the same order as the constants are defined in the the header.
predef startup()#1
predef loadGame()#1
predef newOrLoadGame(ask)#1
predef pressAnyKey()#1
predef gameExists()#1
predef loadInternal()#1
predef newGame(pImportFunc)#1
//AUTOMAP_CHECK// predef checkAutomap()#1
word[] funcTbl = @startup, @loadGame, @newOrLoadGame, @pressAnyKey, @gameExists, @loadInternal, @newGame
//AUTOMAP_CHECK// word           = @_checkAutomap

word pImportModule

// For checking automap mark sizes
include "gen_mapsizes.pla"

///////////////////////////////////////////////////////////////////////////////////////////////////
// Definitions used by assembly code
asm __defs

; Use hi-bit ASCII for Apple II
!convtab "../../include/hiBitAscii.ct"

; Headers
!source "../../include/global.i"
!source "../../include/plasma.i"
!source "../../include/mem.i"
!source "../../include/fontEngine.i"
!source "../../include/prorwts.i"

; Optional debug printing support
DEBUG   = 0

; General use
tmp         = $2
pTmp        = $4

; Yes, needs to be adjusted 3 places.
expandVec   = $200

end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Set up the font engine
asm setFont(pFont)#0
  +asmPlasmNoRet 1
  jmp SetFont
end

///////////////////////////////////////////////////////////////////////////////////////////////////
asm splitExpander()#1   // returns: remaining lo-aux size
!zone {
  +asmPlasmRet 0
.jsr
  jsr fixedRTS
  tsx
  dex
  lda $100,x
  clc
  adc #.splitexp-.jsr-2
  sta pTmp
  lda $101,x
  adc #0
  sta pTmp+1
  ldy #14
- lda (pTmp),y
  sta $10,y
  dey
  bpl -
  jmp $10
.splitexp
!pseudopc $10 {
  sei     ; prevent interrupts while in aux mem
  sta setAuxRd
  jsr +
  sta clrAuxRd
  cli
  rts
+ jmp (expandVec)
} ; end of pseudopc
} ; end of zone
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Relocate the automap mark queue/flush code to aux LC
asm moveMarks(pMarks)#0
  +asmPlasmNoRet 1
  sta setAuxZP
  bit setLcRW+lcBank1
  bit setLcRW+lcBank1
  sta pTmp
  sty pTmp+1
  ldy #0
- lda (pTmp),y
  sta $DB00,y
  iny
  bne -
  inc pTmp+1
- lda (pTmp),y
  sta $DC00,y     ; two pages of code should be plenty
  iny
  bne -
  bit setLcRW+lcBank2
  sta clrAuxZP
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Clear the automap mark bits
asm clearMarkBits(pMarks)#0
  +asmPlasmNoRet 1
  sty pTmp+1
.outer
  sta pTmp
  ldy #0
  lda (pTmp),y  ; get map number
  beq .done     ; if zero, end of maps has been reached
  iny
  lda (pTmp),y  ; map len
  tax
  dex
  dex           ; don't count header in length to clear
  iny
  lda #0
- sta (pTmp),y
  iny
  dex
  bne -
  tya           ; cute way to know total length
  clc
  adc pTmp
  bcc .outer
  inc pTmp+1
  bne .outer    ; always taken
.done
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// It's a little tricky reseeking ProRWTS's file marker
asm reseekRwts#0
    +asmPlasmNoRet 0
    sta setAuxZP
    lda #0
    ldx #4      ; 5 bytes total to clear
-   sta rwts_mark,x
    dex
    bpl -
    sta clrAuxZP
    rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Turn the printer on or off (assumes slot 1)
asm setPrinter(enable)#0
  +asmPlasmNoRet 1
  ldx #<ROM_cout1
  ldy #>ROM_cout1
  asl
  beq +
  ; check slot ID bytes for printer in slot 1. If no printer, don't redirect.
  lda $C105
  cmp #$38
  bne +
  lda $C107
  cmp #$18
  bne +
  lda $C10B
  cmp #$01
  bne +
  lda $C10C
  and #$F0
  cmp #$10  ; printers appear to be $1x
  bne +
  ; redirect cout to the printer
  ldx #0
  ldy #$C1
+ stx cswl
  sty cswh
  rts
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def setRecordMode(enable)#0
  recordMode = enable
  if recordMode; recordSeed = 1; fin
  setPrinter(recordMode)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def decideJace()#0
  word i, a0count

  // Jace is unusual among emulators in that at the paddle locations, it returns a "floating"
  // bus value that reflects the video scanner data.
  a0Count = 0
  ^$C070
  for i = 1 to 100
    if ^$C064 == $A0; a0Count++; fin
  next
  isJace = (a0Count > 50)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Load and display the title screen, set up everything in memory
def startup()#1
  word pEngine, pFont, pMarks, pExpand, expanderSize, pSound

  // Decide if we're running on Jace (if so, later we'll need to credit the music). This has
  // to be done with a mostly blank, text mode, screen.
  decideJace

  puts("Loading game.\n")

  auxMmgr(FREE_MEMORY, $800)  // was temporarily reserved by gameloop to keep diskops bytecode out

  // Allocate and permanently lock mem for the font engine and its font (up in LC ram)
  // Also we can load the sound generator now.
  mmgr(START_LOAD, 1) // partition 1 is where code lives
  mmgr(SET_MEM_TARGET, fontEngine)
  mmgr(REQUEST_MEMORY, fontEngineLen)
  mmgr(LOCK_MEMORY,    fontEngine)

  mmgr(SET_MEM_TARGET, fontData)
  mmgr(REQUEST_MEMORY, fontDataLen)
  mmgr(LOCK_MEMORY,    fontData)

  // Load them into lo mem
  pEngine = mmgr(QUEUE_LOAD, CODE_FONT_ENGINE<<8 | RES_TYPE_CODE)
  pFont   = mmgr(QUEUE_LOAD, 1<<8 | RES_TYPE_FONT)
  pSound  = mmgr(QUEUE_LOAD, CODE_SOUND<<8 | RES_TYPE_CODE)
  mmgr(FINISH_LOAD, 0)

  // Relocate font engine and font data to their final spots up in the language card
  memcpy(pEngine, fontEngine, fontEngineLen, 0)
  memcpy(pFont, fontData, fontDataLen, 0)

  // Tell the font engine where to find its font
  setFont(fontData)

  // Relocate the sound routine to its proper place
  memcpy(pSound, soundCode, soundLen, 0)

  // Load the title screen and show it.
  pResourceIndex = 0 // special case - can't load resourceIndex yet because future expander
  loadFrameImg(1) // title screen is fixed at #1
  ^$C050     // graphics
  ^$C057     // hi-res
  ^$C054     // page 1
  ^$C052     // full screen
  // Hack for real (not emulated) IIc: sometimes displays only lo-bit graphics
  // unless we do this. *HUGE* thanks to Brendan Robert for the fix!
  ^$C07E=0   // disable double-hi-res
  ^$C05F     // disable double-hi-res
  ^EMUSIG_FULL_COLOR

  // While we're loading, let's get the expander into main RAM (we'll copy to aux later)
  pExpand = mmgr(QUEUE_LOAD, CODE_EXPAND<<8 | RES_TYPE_CODE)

  // Also the automap marking queue/flush code.
  pMarks = mmgr(QUEUE_LOAD, CODE_MARKS<<8 | RES_TYPE_CODE)

  // We may need to import a game, so let's load that module now.
  pImportModule = mmgr(QUEUE_LOAD, MOD_IMPORT<<8 | RES_TYPE_MODULE)

  // And the global funcs we'll need for the title screen
  mmgr(QUEUE_LOAD, GS_DISK_LIMIT<<8 | RES_TYPE_MODULE)
  mmgr(QUEUE_LOAD, GS_COPYRIGHT<<8 | RES_TYPE_MODULE)
  mmgr(QUEUE_LOAD, GS_EMU_CREDIT<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)

  // Move the marks code up to its out-of-the-way corner of RAM
  moveMarks(pMarks)

  // And free up the font low mem and marks
  mmgr(FREE_MEMORY, pEngine)
  mmgr(FREE_MEMORY, pFont)
  mmgr(FREE_MEMORY, pMarks)

  // Move the expander to aux low mem (which can't be r/w by ProRWTS), then split it
  // (relocating most of it to aux LC ram)
  memcpy(pExpand, expandVec, expandMax, 1) // 1=target qux
  expanderSize = splitExpander()

  // Done with the main mem copy of the expander
  mmgr(FREE_MEMORY, pExpand)

  // To reduce fragmentation, load the resource index in the lowest possible aux mem spot.
  // Note: this has to be done after the expander, since it temporarily uses this space.
  pResourceIndex = $800
  auxMmgr(SET_MEM_TARGET, pResourceIndex)
  auxMmgr(QUEUE_LOAD, CODE_RESOURCE_INDEX<<8 | RES_TYPE_CODE)
  auxMmgr(LOCK_MEMORY, pResourceIndex)
  mmgr(FINISH_LOAD, 0)

  // Record the disk limit
  diskLimit = callGlobalFunc(GS_DISK_LIMIT, 0, 0, 0)
  if diskLimit >= 8 // Seth likes to set it to 8 to mean "no limit"
    diskLimit = 0
  fin

  // All done.
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def disableSound()#0
  // In record mode, disable sound for quick playback
  ^0 = $60
  memcpy(0, soundCode, 1, 0)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def readDiskMarks()#1
  word size
  // First, read in the marks. Seek past the heap, then read the length
  if callProRWTS(RWTS_SEEK | RWTS_OPENDIR, @S_GAME1_FILENAME, NULL, HEAP_SIZE) <> 0
    fatal("read marks")
  fin
  callProRWTS(RWTS_READ | RWTS_RDWRPART, NULL, LOAD_SAVE_BUF, 2)
  size = *LOAD_SAVE_BUF

  if LOAD_SAVE_BUF+size >= $6000; fatal("mrkSiz"); fin

  // Now that we have the length, read the rest of the mark data
  callProRWTS(RWTS_READ | RWTS_RDWRPART, NULL, LOAD_SAVE_BUF+2, size)
  return size
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Validate the size of each map field in the automap marks buffer
def checkMarks()#0
  word sp, limit
  byte mapNum, fieldLen, err

  // Make an expanded array for quick access to the map sizes
  memset($200, 0, $100)
  for sp = @mapSizes + 1 to @mapSizes + mapSizes[0] - 1 step 2
    ^($200 + ^sp) = ^(sp+1)
  next

  // Now check the buffer size and the size of each field of map marks
  err = 0
  sp = LOAD_SAVE_BUF+2
  limit = min($6000, sp + *LOAD_SAVE_BUF - 1)
  while TRUE
    if sp > limit; err = 'B'; break; fin
    mapNum = ^sp
    if !mapNum; break; fin
    fieldLen = ^(sp+1)
    if fieldLen <> ^($200 + mapNum); err = 'C'; break; fin
    sp = sp + fieldLen
  loop
  if !err and sp <> limit; err = 'D'; fin
  if err; printf1("code=%c\n", err); fatal("corrupt automap"); fin

  // Print out the heap fullness, for later debugging
  printf1("Heap %d%%\n", curHeapPct)
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// TEMPORARY FOR DISK MARK CHECKING
//AUTOMAP_CHECK// def checkAutomap()#1
//AUTOMAP_CHECK//   readDiskMarks; checkMarks
//AUTOMAP_CHECK//   return 0
//AUTOMAP_CHECK// end

///////////////////////////////////////////////////////////////////////////////////////////////////
def loadInternal()#1
  word p_loaded

  // Can be duplicative since gameExists also loaded it, but is needed when re-loading (Ctrl-L)
  rwGame(RWTS_READ)

  // Copy the heap up, and init it with the correct size.
  p_loaded = LOAD_SAVE_BUF
  memcpy(p_loaded, HEAP_BOTTOM, HEAP_SIZE, 0)  // low mem to LC
  initHeap(p_loaded=>w_heapSize)
  heapCollect() // make sure heap is valid, and record final size

  // Check the map marks for valid field sizes
  readDiskMarks; checkMarks

  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def loadGame()#1
  useMapWindow()
  showMapName("Loading game...")

  // Open the file
  ^$4000 = 0  // so 3D engine knows we overwrite HGR page 2, even if we fail
  loadInternal
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def gameExists()#1
  word p_loaded

  // Load save game into mem... the whole thing, so that import code can calc a digest later.
  if callProRWTS(RWTS_READ | RWTS_OPENDIR, @S_GAME1_FILENAME, LOAD_SAVE_BUF, HEAP_SIZE) == 0
    // If heap size is reasonable and type hash matches, chances are high that it's a real saved game.
    p_loaded = LOAD_SAVE_BUF
    if p_loaded=>w_heapSize >= 100 and p_loaded=>w_heapSize <= HEAP_SIZE and p_loaded=>w_typeHash == typeHash
      return TRUE
    fin
  fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def getTextKey()#1
  byte key
  ^$c053
  key = getUpperKey()
  ^$c052
  textHome()
  ^$25 = 20
  return key
end

///////////////////////////////////////////////////////////////////////////////////////////////////
//def soundKey()#1
//  byte key
//  word engine, funcTbl
//  key = getTextKey
//  if key <> 'S'; return key; fin
//  mmgr(START_LOAD, 1) // code is in partition 1
//  engine = mmgr(QUEUE_LOAD, MOD_SNDSEQ<<8 | RES_TYPE_MODULE)
//  mmgr(FINISH_LOAD, 0)
//  funcTbl = engine()
//  ^$c053
//  key = funcTbl=>sndseq_play(1) // temporarily, only 1 song
//  ^$c052
//  textHome()
//  ^$25 = 20
//  return key
//end

///////////////////////////////////////////////////////////////////////////////////////////////////
def pressAnyKey()#1
  puts("\n    and press any key to continue.")
  getTextKey()
  return 0 // dummy value for exported func
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def isNameChar(ch)
  when ch
    is '.'
    is ','
    is '-'
    is '\''
    is ' '
      return TRUE
  wend
  if ch >= 'A' and ch <= 'Z'; return TRUE; fin
  if ch >= '0' and ch <= '9'; return TRUE; fin
  return FALSE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def isValidName(name)
  byte len, c
  len = ^name
  if !len; return FALSE; fin
  if ^(name+1) == ' '; return FALSE; fin // don't allow space at start
  while len > 0
    len--
    name++
    c = charToUpper(^name)
    if c == ' ' and len == 0; return FALSE; fin // don't allow space at end
    if !isNameChar(c); return FALSE; fin
  loop
  return TRUE
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def getCharacterName()#0
  word cursX, cursY
  while TRUE
    displayStr("Character name?\n")
    cursX, cursY = getCursor()
    setWindow(cursY+24, cursY+24+18, cursX+154, cursX+154+62)
    clearWindow()
    global=>p_players=>s_name = getStringResponse()
    setWindow2()
    setCursor(cursX, cursY)
    if isValidName(global=>p_players=>s_name); break; fin
    displayStr("\nInvalid name.\n\n")
    beep()
  loop
end

def getCharacterGender()#0
  byte gender
  repeat
    displayStr("\n\nGender? (M/F/N/...) \n")
    gender = getUpperKey()
    displayChar(gender)
  until gender >= 'A' and gender <= 'Z'
  global=>p_players->c_gender = gender
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def clearDiskMarks()#0
  word size

  // First, read in the marks.
  size = readDiskMarks()
  if callProRWTS(RWTS_SEEK | RWTS_OPENDIR, @S_GAME1_FILENAME, NULL, HEAP_SIZE) <> 0
    fatal("read marks")
  fin
  callProRWTS(RWTS_READ | RWTS_RDWRPART, NULL, LOAD_SAVE_BUF, 2)
  size = *LOAD_SAVE_BUF
  callProRWTS(RWTS_READ | RWTS_RDWRPART, NULL, LOAD_SAVE_BUF+2, size)

  // Clear the fiddly bits
  clearMarkBits(LOAD_SAVE_BUF+2)

  // And write them back
  reseekRwts
  callProRWTS(RWTS_SEEK | RWTS_RDWRPART, NULL, NULL, HEAP_SIZE)
  callProRWTS(RWTS_WRITE | RWTS_RDWRPART, NULL, LOAD_SAVE_BUF, size+2)

  //AUTOMAP_CHECK// _checkAutomap
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def newGame(pImportFunc)#1
  word playersModule, newGameModule, partyModule
  clearDiskMarks()
  global->b_curAvatar = BASE_AVATAR
  global=>w_combatPauseCt = DEFAULT_COMBAT_PAUSE_CT
  global->b_hour = 12  // start at high noon
  global->b_moveMode = 1 // normal
  mmgr(START_LOAD, 1) // players module and new game module both in partition 1
  playersModule = mmgr(QUEUE_LOAD, MOD_GEN_PLAYERS<<8 | RES_TYPE_MODULE)
  partyModule = mmgr(QUEUE_LOAD, MOD_PARTY<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  mmgr(START_LOAD, 2) // new game in partition 2
  newGameModule = mmgr(QUEUE_LOAD, GS_NEW_GAME<<8 | RES_TYPE_MODULE)
  mmgr(FINISH_LOAD, 0)
  playersModule()=>makeInitialParty()
  loadMainFrameImg()
  ^EMUSIG_2D_MAP
  showMapName("New game")
  setWindow2()
  newGameModule()()
  clearWindow()
  if pImportFunc
    pImportFunc()
  else
    getCharacterName()
    getCharacterGender()
  fin
  if global=>p_players->b_skillPoints
    partyModule()=>party_showPlayerSheet(0)
  fin
  heapCollect()
  return 0
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Print version, right-justified in graphics mode and in a protected text area
def printVersion(windowWidth)#0
  word p
  byte len

  buildString()
  if recordMode
    puts("recording")
  else
    puts(diskLimit ?? "Demo" :: "Full")
    puts(" Version ")
    p = pResourceIndex
    len = readAuxByte(p)
    while len
      p++; printChar(readAuxByte(p)); len--
    loop
  fin
  p = finishString(FALSE)

  // And print in graphics mode too
  if windowWidth
    centerStr(p, windowWidth)
  else
      // Save text cursor, and print at bottom of text window
    ^$23 = 24 // full height window
    ^$25 = 22
    crout()
    ^$24 = 39 - ^p // Right-justify on text screen
    puts(p)
  fin
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def displayMenu(existing)#0
  word p

  setWindow(isJace ?? 153 :: 162, 190, 49, 231) // t,b,l,r
  clearWindow
  ^EMUSIG_TITLE
  setWindow(isJace ?? 153 :: 162, 190, 56, 224) // t,b,l,r
  rawDisplayStr("^J\n^Y") // down one line for nice-looking spacing from graphics
  callGlobalFunc(GS_COPYRIGHT, 0, 0 ,0)
  if isJace
    rawDisplayStr("\n")
    callGlobalFunc(GS_EMU_CREDIT, 0, 0, 0)
  fin
  rawDisplayStr("\n^N")
  printVersion(168) // win width

  // Prompt the user for the main game options
  rawDisplayStr("^V001")
  buildString()
  puts("Game: N)ew, ")
  if existing; puts("L)oad, "); fin
  puts("I)mport?")
  p = finishString(FALSE)
  centerStr(p, 160)
  puts(p) // display on text screen too, since Jace looks for Import prompt there.
  crout
end

///////////////////////////////////////////////////////////////////////////////////////////////////
def newOrLoadGame(ask)#1
  byte key, existing
  word ret

  existing = gameExists()
  if existing and !ask
    loadInternal(); return 0
  fin

  // Pause a bit so the player can see the full title screen in all its glory
  pause(2000)

  ^$23 = 24 // Full text window until we're done
  displayMenu(existing)

  // Import functions need a working heap, initially empty
  initHeap(0)

  ret = -1
  while ret < 0

    // Get their choice and act on it
    key = getUpperKey()
    when key
      is 'N'
        clearWindow
        rawDisplayStr("\n^YStarting new game...^N")
        newGame(NULL)
        ret = 1
        break
      is 'L'
        if existing
          clearWindow
          rawDisplayStr("\n^YLoading...^N")
          loadInternal(); ret = 0; break
        else
          beep
        fin
        break
      is 'I'
        clearWindow
        ret = pImportModule()=>import_getGame(@funcTbl)
        if ret == 0
          displayMenu(existing)
          ret = -1 // ask again
        elsif ret == 1
          ret = 0  // imported LL, which is like Loading a game
        else
          ret = 1  // imported Nox, which is like a New game
        fin
        break
      is 'R'
        setRecordMode(1)
        disableSound
        displayMenu(existing)
        break
    wend
  loop

  // Brand future text screens with the version
  printVersion(0) // not on graphics
  ^$23 = 23 // adjust window to protect it
  textHome
  crout // so the very first debug print shows after reset

  // All done
  return ret
end

///////////////////////////////////////////////////////////////////////////////////////////////////
// Boilerplate module initialization code
return @funcTbl
done
