// *** Commonly used functions ***

// Trims whitespace from beginning and end of the specified string.
DEFINE_PATCH_FUNCTION a7_trim
STR_VAR line = ~~
RET line
BEGIN
  INNER_PATCH_SAVE line ~%line%~ BEGIN
    SET max = BUFFER_LENGTH

    // trim start
    FOR (ofs = 0; ofs < max; ++ofs) BEGIN
      READ_BYTE ofs value
      PATCH_IF (value > 0x20) BEGIN
        DELETE_BYTES 0 ofs
        SET max -= ofs
        SET ofs = max
      END
    END

    // trim end
    FOR (ofs = max - 1; ofs >= 0; --ofs) BEGIN
      READ_BYTE ofs value
      PATCH_IF (value > 0x20) BEGIN
        DELETE_BYTES (ofs + 1) (max - ofs - 1)
        SET ofs = "-1"
      END
    END
  END
END


// Adds a "Select Spell" effect to the specified spell ability
DEFINE_PATCH_FUNCTION apply_opcode_214
INT_VAR
  header   = 0
  prob_min = 0
  prob_max = 0
STR_VAR
  resource = ~~
BEGIN
  PATCH_IF (header > 0 && prob_min >= 0 && prob_max <= 100 && prob_min <= prob_max) BEGIN
    LPF ADD_SPELL_EFFECT
    INT_VAR
      opcode = 214    // Select spell
      target = 1      // Self
      power = 1
      parameter2 = 0  // from 2da
      timing = 1      // instant/permanent
      probability1 = prob_max
      probability2 = prob_min
      header = header
      insert_point = 0
    STR_VAR
      resource = EVAL ~%resource%~
    END
  END
END

// Returns first unoccupied spell slot and resref of specified type and level (as SPELL.IDS code), or -1/"" if not available
DEFINE_ACTION_FUNCTION a7_find_free_spell_slot
INT_VAR
  type  = 2
  level = 1
RET
  slot
  resref
BEGIN
  OUTER_SET slot = "-1"
  OUTER_TEXT_SPRINT resref ~~

  ACTION_IF (type >= 1 && type <= 4 && level >= 0 && level < 10) BEGIN
    OUTER_TEXT_SPRINT prefix_1 ~SPPR~
    OUTER_TEXT_SPRINT prefix_2 ~SPWI~
    OUTER_TEXT_SPRINT prefix_3 ~SPIN~
    OUTER_TEXT_SPRINT prefix_4 ~SPCL~

    OUTER_SET range = 49
    OUTER_SET range_min = type * 1000 + level * 100 + 1
    OUTER_SET range_max = range_min + range

    // initializing list of spell slots as unoccupied
    OUTER_FOR (idx = 0; idx < range; ++idx) BEGIN
      OUTER_SET EVAL ~slots_%idx%~ = range_min + idx
    END
    OUTER_SET slots = range

    // marking occupied spell slots
    COPY_EXISTING - ~spell.ids~ ~override~
      READ_2DA_ENTRIES_NOW ~spells~ 2
      FOR (row = 0; row < spells; ++row) BEGIN
        READ_2DA_ENTRY_FORMER ~spells~ row 0 value
        PATCH_IF (IS_AN_INT ~value~ && value >= range_min && value < range_max) BEGIN
          SET idx = value - range_min
          SET EVAL ~slots_%idx%~ = "-1"
        END
      END

    // scanning for first unoccupied slot
    OUTER_FOR (idx = 0; idx < range; ++idx) BEGIN
      ACTION_IF (EVAL ~slots_%idx%~ >= 0) BEGIN
        OUTER_SET slot = EVAL ~slots_%idx%~
        OUTER_SET index = slot - (type * 1000 + level * 100)
        OUTER_TEXT_SPRINT resref EVAL ~%prefix_%type%%~
        ACTION_IF (index < 10) BEGIN
          OUTER_TEXT_SPRINT resref ~%resref%%level%0%index%~
        END ELSE BEGIN
          OUTER_TEXT_SPRINT resref ~%resref%%level%%index%~
        END
        OUTER_SET idx = range
      END
    END
  END
END


// Replaces a specific spell code in SPELL.IDS by a new value
DEFINE_PATCH_FUNCTION a7_update_spell_ids_code
INT_VAR
  code      = 0
  new_code  = 0
BEGIN
  PATCH_IF (code >= 1000 && code < 5000 && 
            new_code >= 1000 && new_code < 5000) BEGIN
    SET ofs = 0
    WHILE (ofs >= 0) BEGIN
      SET ofs = INDEX_BUFFER(~^%code% .*~ ofs)
      PATCH_IF (ofs >= 0) BEGIN
        WRITE_ASCIIE ofs ~%new_code%~ (4)
        SET ofs += 4
      END
    END
  END
END

// Replaces all resref occurrences in the current buffer
DEFINE_PATCH_FUNCTION a7_update_resref
INT_VAR
  terminate = 0   // whether to terminate with a '\0'
STR_VAR
  resref_src = ~~ // search string
  resref_dst = ~~ // replacement string
BEGIN
  PATCH_IF (NOT ~%resref_src%~ STR_EQ ~~) BEGIN
    SET ofs = 0
    SET strlen_src = STRING_LENGTH ~%resref_src%~
    SET strlen_dst = terminate ? 8 : STRING_LENGTH ~%resref_dst%~
    WHILE (ofs >= 0) BEGIN
      SET ofs = INDEX_BUFFER(~%resref_src%~ ofs)
      PATCH_IF (ofs >= 0) BEGIN
        WRITE_ASCIIE ofs ~%resref_dst%~ (strlen_dst)
        SET ofs += 1
      END
    END
  END
END


// Replaces matching parameters of all spell or item effects
DEFINE_PATCH_FUNCTION a7_update_effect_params
INT_VAR
  update_global     = 1   // whether to update global effects
  update_ability    = 1   // whether to update effects in ability structures
  match_parameter1  = 0   // whether to update parameter1 field
  match_parameter2  = 0   // whether to update parameter2 field
  match_special     = 0   // whether to update special field
  match_resource    = 0   // whether to update resource field
  opcode            = 0   // opcode to match
  parameter1        = 0   // parameter1 to match (if match_parameter1 = 1)
  new_parameter1    = 0   // parameter1 replacement if matched
  parameter2        = 0   // parameter2 to match (if match_parameter2 = 1)
  new_parameter2    = 0   // parameter2 replacement if matched
  special           = 0   // special to match (if match_special = 1)
  new_special       = 0   // special replacement if matched
STR_VAR
  resource          = ~~  // resource to match (if match_resource = 1)
  new_resource      = ~~  // resource replacement if matched
BEGIN
  READ_ASCII 0x00 sig (4)
  SET sizeAbil = (~%sig%~ STR_EQ ~ITM ~) ? 0x38 : 0x28
  READ_LONG 0x64 ofsAbil
  SET numAbil = update_ability ? (SHORT_AT 0x68) : 0
  READ_LONG 0x6a ofsEffects
  SET idx = update_global ? "-1" : 0
  FOR (idx = idx; idx < numAbil; ++idx) BEGIN
    // negative index indicates global effects
    PATCH_IF (idx < 0) BEGIN
      READ_SHORT 0x6e firstEffect
      READ_SHORT 0x70 numEffects
    END ELSE BEGIN
      SET curOfsAbil = ofsAbil + (idx * sizeAbil)
      READ_SHORT (curOfsAbil + 0x1e) numEffects
      READ_SHORT (curOfsAbil + 0x20) firstEffect
    END
    FOR (idx2 = 0; idx2 < numEffects; ++idx2) BEGIN
      SET curOfsEffect = ofsEffects + (idx2 + firstEffect) * 0x30
      READ_SHORT (curOfsEffect + 0) curOpcode
      PATCH_IF (curOpcode = opcode) BEGIN
        // matching parameter1
        PATCH_IF (match_parameter1) BEGIN
          READ_LONG (curOfsEffect + 0x04) curParameter1
          PATCH_IF (curParameter1 = parameter1) BEGIN
            WRITE_LONG (curOfsEffect + 0x04) new_parameter1
          END
        END
        // matching parameter2
        PATCH_IF (match_parameter2) BEGIN
          READ_LONG (curOfsEffect + 0x08) curParameter2
          PATCH_IF (curParameter2 = parameter2) BEGIN
            WRITE_LONG (curOfsEffect + 0x08) new_parameter2
          END
        END
        // matching special
        PATCH_IF (match_special) BEGIN
          READ_LONG (curOfsEffect + 0x2c) curSpecial
          PATCH_IF (curSpecial = special) BEGIN
            WRITE_LONG (curOfsEffect + 0x2c) new_special
          END
        END
        // matching resource
        PATCH_IF (match_resource) BEGIN
          READ_ASCII (curOfsEffect + 0x14) curResource (8) NULL
          PATCH_IF (~%curResource%~ STR_EQ ~%resource%~) BEGIN
            WRITE_ASCIIE (curOfsEffect + 0x14) ~%new_resource%~ (8)
          END
        END
      END
    END
  END
END


/**
 * Adds a new entry to a specified IDS file and returns its IDS value.
 * (Taken from Golem Construction mod)
 * 
 * INT_VAR minValue       Minimum IDS value to consider. (Default: 0)
 * INT_VAR maxValue       Maximum IDS value to consider. (Default: 255)
 * INT_VAR preferredValue Try this IDS value first if available. (Default: unset)
 * INT_VAR hexadecimal    Set to nonzero to add IDS value in hexadecimal notation. (Default: 0)
 * STR_VAR idsFile        The IDS file to add the entry to.
 * STR_VAR identifier     The identifier name for the IDS value. Must not contain whitespace.
 * RET value              The IDS value if entry has been added successfully. -1 if entry could not be added.
 */
DEFINE_ACTION_FUNCTION ADD_IDS_ENTRY
INT_VAR
  minValue        = 0
  maxValue        = 255
  preferredValue  = "-1"
  hexadecimal     = 0
STR_VAR
  idsFile         = ""
  identifier      = ""
RET
  value
BEGIN
  OUTER_SET value = "-1"

  ACTION_IF (minValue < 0) BEGIN OUTER_SET minValue = 0 END
  ACTION_IF (maxValue < minValue) BEGIN OUTER_SET maxValue = minValue END

  ACTION_IF (~%idsFile%~ STRING_MATCHES_REGEXP ~.+\..+~ = 0) BEGIN
    OUTER_PATCH_SAVE idsFile ~%idsFile%~ BEGIN REPLACE_TEXTUALLY ~\(.+\)\.[^.]+~ ~\1~ END
  END

  ACTION_IF (FILE_EXISTS_IN_GAME ~%idsFile%.ids~) BEGIN
    // Try preferred value first
    OUTER_PATCH ~~ BEGIN
      PATCH_IF (preferredValue >= minValue AND preferredValue <= maxValue) BEGIN
        LOOKUP_IDS_SYMBOL_OF_INT retVal ~%idsFile%~ preferredValue
        PATCH_IF (~%retVal%~ STRING_EQUAL ~%preferredValue%~) BEGIN
          SET value = preferredValue
        END
      END
    END
    
    // Looking for available IDS slot
    ACTION_IF (value = "-1") BEGIN
      OUTER_PATCH ~~ BEGIN
        FOR (v = minValue; v <= maxValue; v += 1) BEGIN
          LOOKUP_IDS_SYMBOL_OF_INT retVal ~%idsFile%~ v
          PATCH_IF (~%retVal%~ STRING_EQUAL ~%v%~) BEGIN
            SET value = v
            SET v = maxValue + 1
          END
        END
      END
    END

    // Falling back to preferred value if no free slot found
    ACTION_IF (value = "-1" AND preferredValue >= minValue AND preferredValue <= maxValue) BEGIN
      OUTER_SET value = preferredValue
    END

    // Adding new entry
    ACTION_IF (value != "-1") BEGIN
      ACTION_IF (hexadecimal) BEGIN
        LAF TO_HEX_NUMBER INT_VAR value = value RET hexNumber END
        OUTER_TEXT_SPRINT idsValue ~0x%hexNumber%~
      END ELSE BEGIN
        OUTER_TEXT_SPRINT idsValue ~%value%~
      END

      APPEND ~%idsFile%.ids~ ~%idsValue% %identifier%~ UNLESS ~%identifier%~
      OUTER_SET value = IDS_OF_SYMBOL (~%idsFile%~ ~%identifier%~)
      ACTION_IF (value < minValue OR value > maxValue) BEGIN
        OUTER_SET value = "-1"
      END
    END
  END
END


// Removes the line where specified column matches "match"
DEFINE_PATCH_FUNCTION a7_remove_2da_row
INT_VAR column = 0
STR_VAR match  = ~~
BEGIN
  PATCH_IF (NOT ~%match%~ STR_EQ ~~ && column >= 0) BEGIN
    COUNT_2DA_COLS numCols
    PATCH_IF (column < numCols) BEGIN
      COUNT_2DA_ROWS numCols numRows
      SET startRow = 0
      PATCH_IF (numCols < 2) BEGIN SET startRow += 1 END
      PATCH_IF (numCols < 1) BEGIN SET startRow += 1 END
      FOR (row = startRow; row < numRows; ++row) BEGIN
        READ_2DA_ENTRY row column numCols value
        PATCH_IF (~%value%~ STR_EQ ~%match%~) BEGIN
          REMOVE_2DA_ROW row numCols
          SET row = numRows
        END
      END
    END
  END
END


// Returns resref of SPELL.IDS code (e.g. SPWI123), or empty string if not available.
DEFINE_ACTION_FUNCTION a7_resref_of_spell_code
INT_VAR code = 0
RET resref
BEGIN
  OUTER_TEXT_SPRINT resref ~~
  ACTION_IF (code >= 1000 && code < 5000) BEGIN
    OUTER_SET type = code / 1000
    OUTER_SET code = code - (type * 1000)
    ACTION_DEFINE_ARRAY prefix BEGIN ~~ ~SPPR~ ~SPWI~ ~SPIN~ ~SPCL~ END
    OUTER_TEXT_SPRINT resref EVAL ~%prefix_%type%%%code%~
  END
END

DEFINE_PATCH_FUNCTION a7_resref_of_spell_code
INT_VAR code = 0
RET resref
BEGIN
  INNER_ACTION BEGIN
    LAF a7_resref_of_spell_code INT_VAR code = code RET resref END
  END
END


// Automates adding spell effects to items or spells, based on a code sequence
// Code format per effect: "name1=value1,name2=value2;...second effect..."
// Supported parameters:   op (Opcode), tgt (Target), tmg (Timing), pwr (Power), p1 (Parameter1), p2 (Parameter2), 
//                         rd (Resist/Dispel), dur (Duration), pro1 (Probability1), pro2 (Probability2), res (Resource), 
//                         dnum (Dice Number), dsize (Dice Size), stype (Save Type), sbonus (Save Bonus), spec (Special), 
//                         ip (Insert Point), hdr (Header), type (Type)
//                         All parameters except opcode are optional.
// Example:                "op=139,tgt=2,tmg=1,p1=8203;op=215,tgt=2,tmg=1,dur=2,p2=1,res=ICARMOR" adds two effects (139 and 215 with their respective parameters)
DEFINE_PATCH_FUNCTION a7_auto_apply_spl_effect
INT_VAR
  // default values if not specified in "effect_codes"
  def_target        = 0
  def_timing        = 0
  def_power         = 0
  def_parameter1    = 0
  def_parameter2    = 0
  def_resist_dispel = 0
  def_duration      = 0
  def_probability1  = 100
  def_probability2  = 0
  def_dicenumber    = 0
  def_dicesize      = 0
  def_savetype      = 0
  def_savebonus     = 0
  def_special       = 0
  def_insertpoint   = "-1"
  def_header        = 0
  def_type          = 3   // ADD_ITEM_EFFECT only
STR_VAR
  // Supported functions: ADD_SPELL_EFFECT, ADD_SPELL_CFEFFECT, ADD_ITEM_EFFECT, ADD_ITEM_EQEFFECT and (limited) ADD_CRE_EFFECT
  function_name     = ~~
  // The code string with effect definitions
  effect_codes      = ~~
  def_resource      = ~~
BEGIN
  PATCH_IF (NOT ~%function_name%~ STR_EQ ~~) BEGIN
    // parsing effect entries
    SET entries = 0
    SET strlen = STRING_LENGTH ~%effect_codes%~
    INNER_PATCH ~%effect_codes%~ BEGIN
      SET curOfs = 0
      WHILE (curOfs < strlen) BEGIN
        SET ofs = INDEX_BUFFER(~;~ curOfs)
        PATCH_IF (ofs < 0) BEGIN SET ofs = strlen END
        READ_ASCII curOfs entry (ofs - curOfs)
        TEXT_SPRINT EVAL ~entries_%entries%~ ~%entry%~
        SET entries += 1
        SET curOfs = ofs + 1
      END
    END

    // parsing effect parameters
    SET effects = 0
    FOR (idx = 0; idx < entries; ++idx) BEGIN
      TEXT_SPRINT entry EVAL ~%entries_%idx%%~
      SET strlen = STRING_LENGTH ~%entry%~
      INNER_PATCH ~%entry%~ BEGIN
        SET curOfs = 0
        WHILE (curOfs < strlen) BEGIN
          SET ofs = INDEX_BUFFER(~,~ curOfs)
          PATCH_IF (ofs < 0) BEGIN SET ofs = strlen END
          READ_ASCII curOfs param (ofs - curOfs)
          INNER_PATCH ~%param%~ BEGIN
            SET ofs2 = INDEX_BUFFER(~=~)
            PATCH_IF (ofs2 > 0) BEGIN
              READ_ASCII 0 v1 (ofs2)
              READ_ASCII (ofs2+1) v2 (BUFFER_LENGTH - ofs2 - 1)
              TEXT_SPRINT EVAL ~effects_%idx%_%v1%~ ~%v2%~
            END
          END
          SET curOfs = ofs + 1
        END
      END
    END
    SET effects = entries

    // adding effects
    FOR (idx = 0; idx < effects; ++idx) BEGIN
      SET opcode = (VARIABLE_IS_SET EVAL ~effects_%idx%_op~) ? EVAL ~effects_%idx%_op~ : ~-1~
      SET target = (VARIABLE_IS_SET EVAL ~effects_%idx%_tgt~) ? EVAL ~effects_%idx%_tgt~ : def_target
      SET timing = (VARIABLE_IS_SET EVAL ~effects_%idx%_tmg~) ? EVAL ~effects_%idx%_tmg~ : def_timing
      SET power = (VARIABLE_IS_SET EVAL ~effects_%idx%_pwr~) ? EVAL ~effects_%idx%_pwr~ : def_power
      SET param1 = (VARIABLE_IS_SET EVAL ~effects_%idx%_p1~) ? EVAL ~effects_%idx%_p1~ : def_parameter1
      SET param2 = (VARIABLE_IS_SET EVAL ~effects_%idx%_p2~) ? EVAL ~effects_%idx%_p2~ : def_parameter2
      SET resist_dispel = (VARIABLE_IS_SET EVAL ~effects_%idx%_rd~) ? EVAL ~effects_%idx%_rd~ : def_resist_dispel
      SET duration = (VARIABLE_IS_SET EVAL ~effects_%idx%_dur~) ? EVAL ~effects_%idx%_dur~ : def_duration
      SET prob1 = (VARIABLE_IS_SET EVAL ~effects_%idx%_pro1~) ? EVAL ~effects_%idx%_pro1~ : def_probability1
      SET prob2 = (VARIABLE_IS_SET EVAL ~effects_%idx%_pro2~) ? EVAL ~effects_%idx%_pro2~ : def_probability2
      SET dicenumber = (VARIABLE_IS_SET EVAL ~effects_%idx%_dnum~) ? EVAL ~effects_%idx%_dnum~ : def_dicenumber
      SET dicesize = (VARIABLE_IS_SET EVAL ~effects_%idx%_dsize~) ? EVAL ~effects_%idx%_dsize~ : def_dicesize
      SET savetype = (VARIABLE_IS_SET EVAL ~effects_%idx%_stype~) ? EVAL ~effects_%idx%_stype~ : def_savetype
      SET savebonus = (VARIABLE_IS_SET EVAL ~effects_%idx%_sbonus~) ? EVAL ~effects_%idx%_sbonus~ : def_savebonus
      SET special = (VARIABLE_IS_SET EVAL ~effects_%idx%_spec~) ? EVAL ~effects_%idx%_spec~ : def_special
      SET insertpoint = (VARIABLE_IS_SET EVAL ~effects_%idx%_ip~) ? EVAL ~effects_%idx%_ip~ : def_insertpoint
      SET header = (VARIABLE_IS_SET EVAL ~effects_%idx%_hdr~) ? EVAL ~effects_%idx%_hdr~ : def_header
      PATCH_IF (VARIABLE_IS_SET EVAL ~effects_%idx%_res~) BEGIN
        TEXT_SPRINT resource EVAL ~%effects_%idx%_res%~
      END ELSE BEGIN
        TEXT_SPRINT resource ~%def_resource%~
      END
      PATCH_IF (opcode >= 0) BEGIN
        LPF ~%function_name%~
        INT_VAR
          opcode        = opcode
          target        = target
          timing        = timing
          parameter1    = param1
          parameter2    = param2
          power         = power
          resist_dispel = resist_dispel
          duration      = duration
          probability1  = prob1
          probability2  = prob2
          dicenumber    = dicenumber
          dicesize      = dicesize
          savingthrow   = savetype
          savebonus     = savebonus
          special       = special
          insert_point  = insertpoint
          header        = header
        STR_VAR
          resource      = EVAL ~%resource%~
        END
      END
    END
  END
END


// Fixes malformed 2DA files. Can also be used to add new columns.
DEFINE_PATCH_FUNCTION a7_fill_2da_cols
INT_VAR
  expand_header = 1     // Whether to expand header row
  num_cols       = "-1" // Default: Count table columns
STR_VAR
  def_value     = ~~    // Default: Read from table
BEGIN
  PATCH_IF (num_cols < 0) BEGIN
    COUNT_2DA_COLS num_cols
  END

  READ_2DA_ENTRIES_NOW ~tooltip~ 1

  PATCH_IF (~%def_value%~ STR_EQ ~~) BEGIN
    READ_2DA_ENTRY_FORMER ~tooltip~ 1 0 def_value
  END

  PATCH_IF (expand_header) BEGIN
    // processing header
    FOR (col = 0; col < num_cols; ++col) BEGIN
      PATCH_IF (NOT VARIABLE_IS_SET EVAL ~tooltip_2_%col%~) BEGIN
        SET col = num_cols
        READ_2DA_ENTRY_FORMER ~tooltip~ 2 0 line
        FOR (col = 1; col < num_cols - 1; ++col) BEGIN
          PATCH_IF (VARIABLE_IS_SET EVAL ~tooltip_2_%col%~) BEGIN
            READ_2DA_ENTRY_FORMER ~tooltip~ 2 col value
          END ELSE BEGIN
            SET value = col + 1
          END
          TEXT_SPRINT line ~%line% %value%~
        END
        REMOVE_2DA_ROW 2 1
        INSERT_2DA_ROW 2 1 ~%line%~
        SET col = num_cols
      END
    END
  END

  // processing data
  FOR (row = 3; row < tooltip; ++row) BEGIN
    FOR (col = 0; col < num_cols; ++col) BEGIN
      PATCH_IF (NOT VARIABLE_IS_SET EVAL ~tooltip_%row%_%col%~) BEGIN
        READ_2DA_ENTRY_FORMER ~tooltip~ row 0 line
        FOR (col = 1; col < num_cols; ++col) BEGIN
          PATCH_IF (VARIABLE_IS_SET EVAL ~tooltip_%row%_%col%~) BEGIN
            READ_2DA_ENTRY_FORMER ~tooltip~ row col value
          END ELSE BEGIN
            TEXT_SPRINT value ~%def_value%~
          END
          TEXT_SPRINT line ~%line% %value%~
        END
        REMOVE_2DA_ROW row 1
        INSERT_2DA_ROW row 1 ~%line%~
        SET col = num_cols
      END
    END
  END
  PRETTY_PRINT_2DA
END
