'OHRRPGCE CUSTOM - Global text strings editor
'(C) Copyright 1997-2020 James Paige, Ralph Versteegen, and the OHRRPGCE Developers
'Dual licensed under the GNU GPL v2+ and MIT Licenses. Read LICENSE.txt for terms and disclaimer of liability.

#include "config.bi"
#include "udts.bi"
#include "util.bi"
#include "allmodex.bi"
#include "common.bi"
#include "menus.bi"
#include "custom.bi"
#include "customsubs.bi"
#include "loading.bi"
#include "vbcompat.bi"


DECLARE SUB writeglobalstring (index as integer, s as string, maxlen as integer)


TYPE GlobalTextStringsMenu
 index(any) as integer
 descriptions(any) as string
 selectable(any) as bool
 shaded(any) as bool    'For headers
 text(any) as string
 defaults(any) as string
 maxlens(any) as integer
 help(any) as string

 DECLARE CONSTRUCTOR ()

 PRIVATE:
 DECLARE SUB add_item (item_index as integer, description as zstring ptr, default as zstring ptr = @"", maxlen as integer = 0, helpfile as zstring ptr = @"", item_selectable as bool = YES, item_shaded as bool = NO)
 DECLARE SUB header (description as zstring ptr)
END TYPE



SUB GlobalTextStringsMenu.add_item (item_index as integer, description as zstring ptr, default as zstring ptr = @"", maxlen as integer = 0, helpfile as zstring ptr = @"", item_selectable as bool = YES, item_shaded as bool = NO)
 a_append selectable(),   item_selectable
 a_append shaded(),       item_shaded
 a_append index(),        item_index
 a_append descriptions(), *description
 a_append text(),         IIF(item_index >= 0, readglobalstring(item_index, *default, maxlen), *default)
 a_append defaults(),     *default
 a_append maxlens(),      maxlen
 a_append help(),         IIF(LEN(*helpfile), "globalstring_" + *helpfile, "")
END SUB

SUB GlobalTextStringsMenu.header (description as zstring ptr)
 add_item -1, "", , , , NO, YES
 add_item -1, description, , , , NO, YES
END SUB

CONSTRUCTOR GlobalTextStringsMenu ()
 '--load current names

 'getelementnames handles the double-defaulting of element names
 DIM elementnames() as string
 getelementnames elementnames()

 add_item -1,  "Return to Main Menu"

 header        " Stats"
 add_item 0,   "Health Points",              "HP", 10
 add_item 1,   "Spell Points",               "MP", 10
 add_item 2,   "Attack Power",               "Atk", 10
 add_item 3,   "Accuracy",                   "Aim", 10
 add_item 4,   "Extra Hits number",          "Hits", 10
 add_item 5,   "Defense/Blocking Power",     "Def", 10
 add_item 6,   "Dodge",                      "Dodge", 10
 add_item 7,   "Unused stat (aka Counter)",  "Ctr", 10
 add_item 8,   "Speed",                      "Speed", 10
 add_item 29,  "Spell Skill",                "Magic", 10
 add_item 30,  "Spell Defense",              "Will", 10
 add_item 31,  "Spell Cost Reduction %",     "MP~", 10

 header        " Elements"
 FOR i as integer = 0 TO gen(genNumElements) - 1
  add_item 174 + i*2, "Elemental " & i,     elementnames(i), 14
 NEXT i

 header        " Equip slots"
 add_item 38,  "Weapon",                     "Weapon", 10
 add_item 25,  "Armor 1",                    "Head", 10
 add_item 26,  "Armor 2",                    "Body", 10
 add_item 27,  "Armor 3",                    "Arms", 10
 add_item 28,  "Armor 4",                    "Legs", 10

 header        " Special Menu Item Default Captions"
 add_item 60,  "Items",                      "Items", 10
 add_item 61,  "Spells",                     "Spells", 10
 add_item 62,  "Status",                     "Status", 10
 add_item 63,  "Equip",                      "Equip", 10
 add_item 64,  "Order",                      "Order", 10
 add_item 65,  "Team",                       "Team", 10
 add_item 66,  "Save",                       "Save", 10
 add_item 322, "Load",                       "Load", 20
 add_item 67,  "Quit",                       "Quit", 10
 add_item 68,  "Minimap",                    "Map", 10
 add_item 69,  "Volume Menu [obsolete]",     "Volume", 10  'Obsolete
 add_item 318, "Music Volume",               "Music", 20
 add_item 320, "Sound Volume",               "Sound", 20
 add_item 308, "TV Margins [consoles only]", "Margins", 10
 add_item 313, "In-App Purchases",           "Purchases", 10
 add_item 314, "Switch to windowed",         "Windowed", 20
 add_item 316, "Switch to fullscreen",       "Fullscreen", 20

 header        " Item Menu"
 add_item 35,  "Exit Item Menu",             "DONE", 10
 add_item 36,  "Sort Item Menu",             "AUTOSORT", 10
 add_item 37,  "Drop Item",                  "TRASH", 10
 add_item 41,  "Drop Prompt",                "Discard", 10
 add_item 42,  "Negative Drop Prefix",       "Cannot", 10

 header        " Status Main Screen"
 add_item 43,  "Level",                      "Level", 10
 add_item 33,  "Experience",                 "Experience", 10
 add_item 47,  "(exp) for next (level)",     "for next", 10
 DIM moneydefault as string = "# " & readglobalstring(32, "$")
 IF moneydefault = "# $" THEN moneydefault = "$#"  'Special case magic!
 add_item 330, "Money/Price",                moneydefault, 16, "money"
 add_item 160, "Level MP",                   "Level MP", 20

 header        " Status Second Screen"
 add_item 302, "Elemental Effects Title",    "Elemental Effects:", 30
 add_item 130, "No Elemental Effects",       "No Elemental Effects", 30
 add_item 162, "Takes > 100% element dmg",   "Weak to $E", 30,   "elemental_resist"
 add_item 165, "Takes 0 to 100% element dmg","Strong to $E", 30, "elemental_resist"
 add_item 168, "Takes 0% element dmg",       "Immune to $E", 30, "elemental_resist"
 add_item 171, "Takes < 0% element dmg",     "Absorb $E", 30,   "elemental_resist"

 header        " Equip Menu"
 add_item 110, "Equip Nothing (unequip)",    "Nothing", 10
 add_item 39,  "Unequip All",                "-REMOVE-", 8
 add_item 40,  "Exit Equip",                 "-EXIT-", 8

 header        " Spells Menu"
 add_item 133, "(hero) has no spells",       "has no spells", 20
 add_item 46,  "Exit Spell List Menu",       "EXIT", 10
 add_item 51,  "Cancel Spell Menu",          "(CANCEL)", 10

 header        " Team/Order Menu"
 add_item 48,  "Remove Hero from Team",      "REMOVE", 10

 header        " Save/Load Menus"
 add_item 52,  "New Game",                   "NEW GAME", 10
 add_item 53,  "Exit Game",                  "EXIT", 10
 add_item 59,  "Cancel Save",                "CANCEL", 10
 add_item 102, "Replace Save Prompt",        "Replace Old Data?", 20
 add_item 44,  "Overwrite Save Yes",         "Yes", 10
 add_item 45,  "Overwrite Save No",          "No", 10
 add_item 154, "day",                        "day", 10
 add_item 155, "days",                       "days", 10
 add_item 156, "hour",                       "hour", 10
 add_item 157, "hours",                      "hours", 10
 add_item 158, "minute",                     "minute", 10
 add_item 159, "minutes",                    "minutes", 10

 header        " Quit Playing Prompt"
 add_item 55,  "Prompt",                     "Quit Playing?", 20
 add_item 57,  "Yes",                        "Yes", 10
 add_item 58,  "No",                         "No", 10

 header        " Shop Menu"
 add_item 70,  "Buy",                        "Buy", 10
 add_item 71,  "Sell",                       "Sell", 10
 add_item 72,  "Inn",                        "Inn", 10
 add_item 73,  "Hire",                       "Hire", 10
 add_item 74,  "Exit",                       "Exit", 10
 add_item 324, "You own (itemcount)",        "You own", 20
 add_item 326, "Equipped (itemcount)",       "Equipped", 20

 header        " Buy/Hire Menu"
 add_item 85,  "Trade for (items, no $)",    "Trade for", 20, "trade_for_prefix"
 add_item 328, "Trade-in amount you have",   " (have $N)", 20, "trade_item_owned"
 add_item 87,  "Hire price prefix",          "Joins for", 20
 add_item 97,  "(#) in stock",               "in stock", 20
 add_item 99,  "Equipability prefix",        "Equip:", 10
 add_item 89,  "Cannot buy prefix",          "Cannot Afford", 20
 add_item 91,  "Cannot hire prefix",         "Cannot Hire", 20
 add_item 305, "Inventory full warning",     "No room in inventory", 30
 add_item 100, "Party full warning",         "No room in party", 20
 add_item 93,  "Buy alert",                  "Purchased", 20
 add_item 95,  "Hire alert (suffix)",        "Joined!", 20
 add_item 309, "The shop is empty",          "The shop is empty", 30

 header        " Sell Menu"
 add_item 77,  "Value: Worth ($) (and...)",  "Worth", 20
 add_item 81,  "($) and a (item)",           "and a", 10
 add_item 153, "($) and (number) (item)",    "and", 10
 add_item 79,  "Value: Trade for (item)",    "Trade for", 20
 add_item 82,  "Worthless item warning",     "Worth Nothing", 20
 add_item 75,  "Unsellable item warning",    "CANNOT SELL", 20
 add_item 84,  "Sell alert",                 "Sold", 10

 header        " Inns"
 add_item 143, "THE INN COSTS ($)",          "THE INN COSTS", 20
 add_item 145, "You have ($)",               "You have", 20
 add_item 49,  "Pay at Inn",                 "Pay", 10
 add_item 50,  "Cancel Inn",                 "Cancel", 10

 header        " Battles"
 add_item 34,  "Battle Item Menu",           "Item", 10
 add_item 117, "Stole (itemname)",           "Stole", 30
 add_item 111, "Nothing to Steal",           "Has Nothing", 30
 add_item 114, "Steal Failure",              "Cannot Steal", 30
 add_item 120, "When an Attack Misses",      "miss", 20
 add_item 122, "When a Spell Fails",         "fail", 20
 add_item 147, "CANNOT RUN!",                "CANNOT RUN!", 20
 add_item 54,  "Pause",                      "PAUSE", 10
 add_item 126, "Gained (experience)",        "Gained", 10
 add_item 149, "Level up for (hero)",        "Level up for", 20
 add_item 151, "(#) levels for (hero)",      "levels for", 20
 add_item 124, "(hero) learned (spell)",     "learned", 10
 add_item 139, "Found a (item)",             "Found a", 20
 add_item 141, "Found (number) (items)",     "Found", 20
 add_item 125, "Found ($)",                  "Found", 10
 add_item 332, "Skip Turn in battle",        "Skip Turn", 20

 header        " Misc"
 add_item 104, "Status Prompt",              "Whose Status?", 20
 add_item 106, "Spells Prompt",              "Whose Spells?", 20
 add_item 108, "Equip Prompt",               "Equip Who?", 20
 add_item 135, "Plotscript: pick hero",      "Which Hero?", 20
 add_item 137, "Hero name prompt",           "Name the Hero", 20

 '**** next unused index is 332

 'NOTE: if you add global strings here, technically you should update the limit-checking on
 'the implementation of the "get global string" plotscripting command
 '(but that command is actually undocumented and unusable, so it doesn't matter)
END CONSTRUCTOR

SUB global_text_strings_editor()
 DIM search as string = ""
 DIM state as MenuState
 DIM as MenuOptions menuopts, menuopts2
 DIM menu as GlobalTextStringsMenu

 WITH state
  .last = UBOUND(menu.text)
  .autosize = YES
  .autosize_ignore_lines = 3
 END WITH
 setkeys YES
 DO
  setwait 55
  setkeys YES
  IF keyval(ccCancel) > 1 THEN EXIT DO
  IF keyval(scF1) > 1 THEN
   IF LEN(menu.help(state.pt)) THEN
    show_help menu.help(state.pt)
   ELSE
    show_help "edit_global_strings"
   END IF
  END IF
  IF keyval(scCTRL) > 0 AND keyval(scS) > 1 THEN
   IF prompt_for_string(search, "Search (descriptions & values)") THEN
    'Search forward from state.pt
    FOR i as integer = 0 TO state.last
     DIM idx as integer = state.pt + 1 + i
     IF idx > state.last THEN idx = idx - state.last + 1

     IF INSTR(LCASE(menu.text(idx)), LCASE(search)) OR INSTR(LCASE(menu.descriptions(idx)), LCASE(search)) THEN
      state.pt = idx
      correct_menu_state state
      EXIT FOR
     END IF
    NEXT i
   END IF
  END IF
  IF keyval(scCTRL) > 0 AND keyval(scD) > 1 THEN
   'Revert to default
   IF LEN(menu.defaults(state.pt)) THEN
    menu.text(state.pt) = menu.defaults(state.pt)
   END IF
  END IF

  usemenu state, menu.selectable()
  IF state.pt = 0 THEN
   IF enter_space_click(state) THEN EXIT DO
  ELSEIF menu.index(state.pt) <> -1 THEN
   strgrabber menu.text(state.pt), menu.maxlens(state.pt)
  END IF

  clearpage dpage
  menuopts.highlight_selection = (state.pt <> 0) 'Don't hightlight Return to Main Menu
  standardmenu menu.text(), state, pMenuX + 232, , dpage, menuopts
  'Since both halves of the menu share the same state and both are active,
  'they both toggle state.tog. So work around that
  state.tog XOR= 1

  menuopts2.disabled_col = uilook(eduiHeading)
  standardmenu menu.descriptions(), state, menu.shaded(), , , dpage, menuopts2
  edgeprint "CTRL+S Search", pInfoX, pInfoY, uilook(uiDisabledItem), dpage
  IF LEN(menu.defaults(state.pt)) THEN
   edgeprint "Default: " & menu.defaults(state.pt), pInfoRight, pInfoY, uilook(uiDisabledItem), dpage
  END IF
  IF LEN(menu.help(state.pt)) THEN
   edgeprint "Press F1 for help", pInfoX, pInfoY - 9, uilook(uiDisabledItem), dpage
  END IF
  IF menu.index(state.pt) >= 0 THEN
   edgeboxstyle rCenter - (menu.maxlens(state.pt) * 4), pBottom, 8 * menu.maxlens(state.pt) + 4, 10, 0, dpage, transOpaque, YES
   edgeprint menu.text(state.pt), rCenter + 2 - (menu.maxlens(state.pt) * 4), pBottom + 1, uilook(uiText), dpage
  END IF
  SWAP vpage, dpage
  setvispage vpage
  dowait
 LOOP

 'Note: it is safe to write the strings to file out of order as long as we write
 'all of them. Any gaps in the file will be filled with garbage: do not leave
 'unused global string indices, or you won't be able to use them later!
 FOR i as integer = 0 TO UBOUND(menu.index)
  writeglobalstring menu.index(i), menu.text(i), menu.maxlens(i)
 NEXT i
 'Write defaults for all elements that don't appear in the menu
 FOR i as integer = gen(genNumElements) TO 63 'maxElements - 1
  writeglobalstring 174 + i*2, "Element" & i+1, 14
 NEXT i

 getstatnames statnames()
END SUB

SUB writeglobalstring (index as integer, s as string, maxlen as integer)
 IF index < 0 THEN EXIT SUB
 DIM fh as integer
 OPENFILE(game & ".stt", FOR_BINARY, fh)
 DIM ch as string
 ch = CHR(small(LEN(s), small(maxlen, 255)))
 PUT #fh, 1 + index * 11, ch
 ch = LEFT(s, small(maxlen, 255))
 PUT #fh, 2 + index * 11, ch
 CLOSE #fh
 loadglobalstrings
END SUB

'==========================================================================================
'                                       Translations
'==========================================================================================


FUNCTION escape_str_for_web_translation(istr as string) as string
 DIM result as string = ""
 DIM as integer icons_low, icons_high
 IF get_font_type(current_font()) = ftypeLatin1 THEN
  icons_low = 127
  icons_high = 160
 ELSE
  icons_low = 127
  icons_high = 255
 END IF
 FOR idx as integer = 1 TO LEN(istr)
  DIM ch as string = MID(istr, idx, 1)
  SELECT CASE CAST(integer, ASC(ch))
   CASE 10  'New line
    result &= LINE_END
   CASE 0 TO 31, icons_low TO icons_high
    result &= "${*" & ASC(ch) & "}"
   CASE ELSE
    result &= ch
  END SELECT
 NEXT idx
 RETURN result
END FUNCTION

'Function for sorting
LOCAL FUNCTION compare_translations(a as HashBucketItem ptr, b as HashBucketItem ptr) as integer
  #DEFINE item_sortorder(x) CAST(TranslationString ptr, x->value)->sortorder
  RETURN item_sortorder(a) - item_sortorder(b)
END FUNCTION

SUB write_translation_file_txt(fname as string, translations as StrHashTable)
  DIM fh as integer
  IF OPENFILE(fname, FOR_OUTPUT + OR_ERROR, fh) THEN EXIT SUB

  PRINT #fh, "# Translation format 0  |  <  '''$ { X }'''  *"
  PRINT #fh, "# Translations for " & trimpath(sourcerpg)
  PRINT #fh, "# Exported " & format_date(NOW)  'FILEDATETIME(sourcerpg))

  DIM items as HashBucketItem vector = translations.items()
  v_sort items, CAST(FnCompare, @compare_translations)

  FOR idx as integer = 0 TO v_len(items) - 1
    DIM code as string ptr = items[idx].key
    WITH *CAST(TranslationString ptr, items[idx].value)
      PRINT #fh,
      IF LEN(.description) THEN PRINT #fh, "# " & .description
      IF .maxlen THEN PRINT #fh, SPACE(LEN(*code) + 4) & STRING(.maxlen, "=")
      DIM tokens as string

      'Convert leading spaces to a WS token
      DIM spaces as integer = skip_over(.text, 1, " ")
      IF spaces THEN tokens &= " ws" & spaces

      IF INSTR(.text, !"\n") THEN
        PRINT #fh, *code & tokens & " <" & LINE_END & escape_str_for_web_translation(.text) & LINE_END & *code & " >"
      ELSE
        PRINT #fh, *code & tokens & " | " & escape_str_for_web_translation(.text)
      END IF
    END WITH
  NEXT

  CLOSE fh

  v_free items
END SUB

'This is a variant on textbox_lines_to_string()
FUNCTION unwrap_textbox(box as TextBox) as string
  DIM ret as string

  CONST leeway as integer = 4  'In characters
  DIM rightmargin as integer = 0  'In characters: space at right of the textbox

  debug "box_to_string"

  FOR idx as integer = 0 TO text_box_last_line(box)
    DIM lin as string = box.text(idx)
    DIM indentation as integer = skip_over(lin, 1, " ")
    DIM unwrap as bool = NO

    IF idx > 0 THEN
      'Heuristic test for whether this line is a continuation of the previous one,
      'in which case they should be joined without a newline.
      'Do this because it greatly confuses automated translators to have spurious newlines,
      'and because we will need to re-wrap the lines anyway.

      DIM firstwordend as integer = INSTR(1 + indentation, lin, " ")
      IF firstwordend = 0 THEN firstwordend = LEN(lin) + 1
      DIM firstwordlen as integer = firstwordend - (1 + indentation)

      'Cap length of the word, because if it's really long then 1) you likely would
      'split it with hyphens if you were wrapping text, 2) it's likely not a word
      firstwordlen = small(firstwordlen, 12)

      debug "firstword " & MID(lin, 1 + indentation, firstwordlen)

      IF LEN(RTRIM(box.text(idx - 1))) + 1 + firstwordlen > 38 - rightmargin - leeway THEN
        'If leeway is 0, this word couldn't have fit on the previous line.
        'But all a little leeway because people often wrap their lines early.
        ret &= " "
      ELSE
        ret &= !"\n"
      END IF
    END IF
    ret &= TRIM(lin)
  NEXT
  RETURN ret
END FUNCTION

SUB create_translations()
  init_translations

  'Textboxes are tricky because the text need to be unwrapped and rewrapped
  FOR id as integer = 0 TO gen(genMaxTextBox)
    DIM box as TextBox
    LoadTextBox box, id
    DIM text as string = unwrap_textbox(box)
    add_translation "tb" & id, text,
    IF box.choice_enabled THEN
      FOR idx as integer = 0 TO UBOUND(box.choice)
        add_translation "tb" & id & "c" & idx, box.choice(idx), "Text box " & id & " choice " & idx
      NEXT
    END IF
  NEXT
END SUB

FUNCTION export_translations (fname as string) as bool
  create_translations
  write_translation_file_txt(fname, translations)
  show_overlay_message "Wrote " & fname
  RETURN YES
END FUNCTION

SUB translations_menu ()
  export_translations "translations.txt"
END SUB