if not WeakAuras.IsLibsOK() then return end
--- @type string, Private
local AddonName, Private = ...

local L = WeakAuras.L
local timer = WeakAuras.timer

-- Dynamic Condition functions to run. keyed on event and uid
local dynamicConditions = {};

-- Global Dynamic Condition Funcs, keyed on the event
local globalDynamicConditionFuncs = {};

-- Check Conditions Functions, keyed on uid
local checkConditions = {};

local conditionChecksTimers = {};
conditionChecksTimers.recheckTime = {};
conditionChecksTimers.recheckHandle = {};

local function OnDelete(_, uid)
  checkConditions[uid] = nil
  conditionChecksTimers.recheckTime[uid] = nil
  if (conditionChecksTimers.recheckHandle[uid]) then
    for cloneId, v in pairs(conditionChecksTimers.recheckHandle[uid]) do
      timer:CancelTimer(v)
    end
  end
  conditionChecksTimers.recheckHandle[uid] = nil

  for _, funcs in pairs(dynamicConditions) do
    funcs[uid] = nil
  end
end

Private.callbacks:RegisterCallback("Delete", OnDelete)

local function formatValueForAssignment(vType, value, pathToCustomFunction, pathToFormatters)
  if (value == nil) then
    value = false;
  end
  if (vType == "bool") then
    return value and tostring(value) or "false";
  elseif(vType == "number") then
    return value and tostring(value) or "0";
  elseif (vType == "list") then
    if type(value) == "string" then
      return string.format("%s", Private.QuotedString(value))
    elseif type(value) == "number" then
      return tostring(value)
    end
    return "nil"
  elseif (vType == "icon") then
    if type(value) == "string" then
      return string.format("%s", Private.QuotedString(value))
    elseif type(value) == "number" then
      return tostring(value)
    end
    return "nil"
  elseif (vType == "string") then
    if type(value) == "string" then
      return string.format("%s", Private.QuotedString(value))
    end
    return "nil"
  elseif(vType == "color") then
    if (value and type(value) == "table") then
      return string.format("{%s, %s, %s, %s}",
                           tostring(value[1]), tostring(value[2]),
                           tostring(value[3]), tostring(value[4]))
    end
    return "{1, 1, 1, 1}";
  elseif(vType == "chat") then
    if (value and type(value) == "table") then
      local serialized = string.format("{message_type = %s, message = %s, message_dest = %s, message_dest_isunit = %s, r = %s, g = %s, b = %s, message_custom = %s, message_formaters = %s, message_voice = %s}",
        Private.QuotedString(tostring(value.message_type)), Private.QuotedString(tostring(value.message or "")),
        Private.QuotedString(tostring(value.message_dest)),
        tostring(value.message_dest_isunit),
        type(value.message_color) == "table" and tostring(value.message_color[1] or "1") or "1",
        type(value.message_color) == "table" and tostring(value.message_color[2] or "1") or "1",
        type(value.message_color) == "table" and tostring(value.message_color[3] or "1") or "1",
        pathToCustomFunction,
        pathToFormatters,
        tostring(value.message_voice))
      return serialized
    end
  elseif(vType == "sound") then
    if (value and type(value) == "table") then
      return string.format("{ sound = %s, sound_channel = %s, sound_path = %s, sound_kit_id = %s, sound_type = %s, %s}",
        Private.QuotedString(tostring(value.sound or "")),
        Private.QuotedString(tostring(value.sound_channel or "")),
        Private.QuotedString(tostring(value.sound_path or "")),
        Private.QuotedString(tostring(value.sound_kit_id or "")),
        Private.QuotedString(tostring(value.sound_type or "")),
        value.sound_repeat and "sound_repeat = " .. tostring(value.sound_repeat) or "nil");
    end
  elseif(vType == "customcode") then
    return string.format("%s", pathToCustomFunction);
  elseif vType == "glowexternal" then
    if (value and type(value) == "table") then
      return ([[{ glow_action = %q, glow_frame_type = %q, glow_type = %q,
      glow_frame = %q, use_glow_color = %s, glow_color = {%s, %s, %s, %s},
      glow_startAnim = %s, glow_duration = %f, glow_lines = %d, glow_frequency = %f, glow_length = %f, glow_thickness = %f, glow_XOffset = %f, glow_YOffset = %f,
      glow_scale = %f, glow_border = %s }]]):format(
        value.glow_action or "",
        value.glow_frame_type or "",
        value.glow_type or "",
        value.glow_frame or "",
        value.use_glow_color and "true" or "false",
        type(value.glow_color) == "table" and tostring(value.glow_color[1]) or "1",
        type(value.glow_color) == "table" and tostring(value.glow_color[2]) or "1",
        type(value.glow_color) == "table" and tostring(value.glow_color[3]) or "1",
        type(value.glow_color) == "table" and tostring(value.glow_color[4]) or "1",
        value.glow_startAnim and "true" or "false",
        value.glow_duration or 1,
        value.glow_lines or 8,
        value.glow_frequency or 0.25,
        value.glow_length or 10,
        value.glow_thickness or 1,
        value.glow_XOffset or 0,
        value.glow_YOffset or 0,
        value.glow_scale or 1,
        value.glow_border and "true" or "false"
      )
    end
  end
  return "nil";
end

local function formatValueForCall(type, property)
  if (type == "bool" or type == "number" or type == "list" or type == "icon" or type == "string") then
    return "propertyChanges['" .. property .. "']";
  elseif (type == "color") then
    local pcp = "propertyChanges['" .. property .. "']";
    return pcp  .. "[1], " .. pcp .. "[2], " .. pcp  .. "[3], " .. pcp  .. "[4]";
  end
  return "nil";
end


function Private.ExecEnv.CancelConditionCheck(uid, cloneId)
  if conditionChecksTimers.recheckHandle[uid] and conditionChecksTimers.recheckHandle[uid][cloneId] then
    timer:CancelTimer(conditionChecksTimers.recheckHandle[uid][cloneId])
    conditionChecksTimers.recheckHandle[uid][cloneId] = nil
  end
end

function Private.ExecEnv.ScheduleConditionCheck(time, uid, cloneId)
  conditionChecksTimers.recheckTime[uid] = conditionChecksTimers.recheckTime[uid] or {}
  conditionChecksTimers.recheckHandle[uid] = conditionChecksTimers.recheckHandle[uid] or {};

  if (conditionChecksTimers.recheckTime[uid][cloneId] and conditionChecksTimers.recheckTime[uid][cloneId] > time) then
    timer:CancelTimer(conditionChecksTimers.recheckHandle[uid][cloneId]);
    conditionChecksTimers.recheckHandle[uid][cloneId] = nil;
  end

  if (conditionChecksTimers.recheckHandle[uid][cloneId] == nil) then
    conditionChecksTimers.recheckHandle[uid][cloneId] = timer:ScheduleTimerFixed(function()
      conditionChecksTimers.recheckHandle[uid][cloneId] = nil;
      local region = Private.GetRegionByUID(uid, cloneId)
      if (region and region.toShow) then
        Private.ActivateAuraEnvironmentForRegion(region)
        checkConditions[uid](region);
        Private.ActivateAuraEnvironment()
      end
    end, time - GetTime())
    conditionChecksTimers.recheckTime[uid][cloneId] = time;
  end
end

function Private.ExecEnv.CallCustomConditionTest(uid, testFunctionNumber, ...)
  local ok, result = xpcall(Private.ExecEnv.conditionHelpers[uid].customTestFunctions[testFunctionNumber],
                            Private.GetErrorHandlerUid(uid, L["Condition Custom Text"]), ...)
  if (ok) then
    return result
  end
end

local function CreateTestForCondition(uid, input, allConditionsTemplate, usedStates)
  local trigger = input and input.trigger;
  local variable = input and input.variable;
  local op = input and input.op;
  local value = input and input.value;

  local check = nil;
  local recheckCode = nil;

  if (variable == "AND" or variable == "OR") then
    local test = {};
    if (input.checks) then
      for i, subcheck in ipairs(input.checks) do
        local subtest, subrecheckCode = CreateTestForCondition(uid, subcheck, allConditionsTemplate, usedStates);
        if (subtest) then
          tinsert(test, "(" .. subtest .. ")");
        end
        if (subrecheckCode) then
          recheckCode = recheckCode or "";
          recheckCode = recheckCode .. subrecheckCode;
        end
      end
    end
    if (next(test)) then
      if (variable == "AND") then
        check = table.concat(test, " and ");
      else
        check = table.concat(test, " or ");
      end
    end
  end

  if (trigger and variable) then
    usedStates[trigger] = true;

    local conditionTemplate = allConditionsTemplate[trigger] and allConditionsTemplate[trigger][variable];
    local cType = conditionTemplate and conditionTemplate.type;
    local useModRate = conditionTemplate and conditionTemplate.useModRate
    local test = conditionTemplate and conditionTemplate.test;
    local preamble = conditionTemplate and conditionTemplate.preamble;

    local stateCheck = "state[" .. trigger .. "] and state[" .. trigger .. "].show and ";
    local stateVariableCheck = string.format("state[" .. trigger .. "][%q]", variable) .. "~= nil and ";

    local preambleString

    if preamble then
      Private.ExecEnv.conditionHelpers[uid] = Private.ExecEnv.conditionHelpers[uid] or {}
      Private.ExecEnv.conditionHelpers[uid].preambles = Private.ExecEnv.conditionHelpers[uid].preambles or {}
      tinsert(Private.ExecEnv.conditionHelpers[uid].preambles, preamble(value) or "");
      local preambleNumber = #Private.ExecEnv.conditionHelpers[uid].preambles
      preambleString = string.format("Private.ExecEnv.conditionHelpers[%q].preambles[%s]", uid, preambleNumber)
    end

    if (test) then
      if (value) then
        Private.ExecEnv.conditionHelpers[uid] = Private.ExecEnv.conditionHelpers[uid] or {}
        Private.ExecEnv.conditionHelpers[uid].customTestFunctions
          = Private.ExecEnv.conditionHelpers[uid].customTestFunctions or {}
        tinsert(Private.ExecEnv.conditionHelpers[uid].customTestFunctions, test);
        local testFunctionNumber = #(Private.ExecEnv.conditionHelpers[uid].customTestFunctions);
        local valueString = type(value) == "string" and string.format("%q", value) or value;
        local opString = type(op) == "string" and string.format("%q", op) or op;
        check = string.format("state and Private.ExecEnv.CallCustomConditionTest(%q, %s, state[%s], %s, %s, %s)",
                              uid, testFunctionNumber, trigger, valueString, (opString or "nil"),
                              preambleString or "nil")
      end
    elseif (cType == "customcheck") then
      if value then
        local customCheck = WeakAuras.LoadFunction("return " .. value)
        if customCheck then
          Private.ExecEnv.conditionHelpers[uid] = Private.ExecEnv.conditionHelpers[uid] or {}
          Private.ExecEnv.conditionHelpers[uid].customTestFunctions
            = Private.ExecEnv.conditionHelpers[uid].customTestFunctions or {}
          tinsert(Private.ExecEnv.conditionHelpers[uid].customTestFunctions, customCheck);
          local testFunctionNumber = #(Private.ExecEnv.conditionHelpers[uid].customTestFunctions);

          check = string.format("state and Private.ExecEnv.CallCustomConditionTest(%q, %s, state)",
                                uid, testFunctionNumber, trigger);
        end
      end
    elseif cType == "alwaystrue" then
      check = "true"
    elseif (cType == "number" and value and op) then
      local v = tonumber(value)

      if (v) then
        if useModRate then
          check = stateCheck .. stateVariableCheck .. "(state[" .. trigger .. "]" .. string.format("[%q]", variable)
                  .. "/ (state[" .. trigger .. "].modRate or 1.0))" .. op .. v;
        else
          check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]", variable)
                  .. op .. v;
        end
      end
    elseif (cType == "timer" and value and op) then
      local triggerState = "state[" .. trigger .. "]"
      local varString = triggerState .. string.format("[%q]", variable)
      local remainingTime = "(" .. triggerState .. ".paused and (" .. triggerState .. ".remaining or 0) or (" .. varString .. " - now)" .. ")"
      if useModRate then
        local modRateString = "(state[" .. trigger .. "].modRate or 1.0)"

        if (op == "==") then
          check = stateCheck .. stateVariableCheck .. "abs((" .. remainingTime .. "-" .. value .. ")/" .. modRateString .. ") < 0.05"
        else
          check = stateCheck .. stateVariableCheck .. remainingTime .. "/" .. modRateString .. op .. value
        end
      else
        if (op == "==") then
          check = stateCheck .. stateVariableCheck .. "abs(" .. remainingTime .. "-" .. value .. ") < 0.05"
        else
          check = stateCheck .. stateVariableCheck .. remainingTime .. op .. value
        end
      end
    elseif (cType == "elapsedTimer" and value and op) then
      if (op == "==") then
        check = stateCheck .. stateVariableCheck .. "abs(state[" .. trigger .. "]" .. string.format("[%q]", variable)
                .. "- now +" .. value .. ") < 0.05";
      else
        check = stateCheck .. stateVariableCheck .. "now - state[" .. trigger .. "]" .. string.format("[%q]", variable)
                .. op .. value;
      end
    elseif (cType == "select" and value and op) then
      if (tonumber(value)) then
        check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]", variable)
                .. op .. tonumber(value);
      else
        check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]".. string.format("[%q]", variable)
                .. op .. "'" .. value .. "'";
      end
    elseif (cType == "range" and value and op and input.type and input.op_range and input.range) then
      local fn
      if input.type == "group" then
        fn = [[
          return function()
            local found = 0
            local op = %q
            local range = %s
            for unit in WA_IterateGroupMembers() do
              if not UnitIsUnit(unit, "player") and WeakAuras.CheckRange(unit, range, op) then
                found = found + 1
              end
            end
            return found %s %d
          end
        ]]
        fn = fn:format(input.op_range, input.range, op, value)
      elseif input.type == "enemies" then
        fn = [[
          return function()
            local found = 0
            local op = %q
            local range = %s
            for i = 1, 40 do
              local unit = "nameplate" .. i
              if UnitExists(unit) and UnitCanAttack("player", unit) and WeakAuras.CheckRange(unit, range, op) then
                found = found + 1
              end
            end
            return found %s %d
          end
        ]]
        fn = fn:format(input.op_range, input.range, op, value)
      end
      if fn then
        local customCheck = WeakAuras.LoadFunction(fn)
        if customCheck then
          Private.ExecEnv.conditionHelpers[uid] = Private.ExecEnv.conditionHelpers[uid] or {}
          Private.ExecEnv.conditionHelpers[uid].customTestFunctions
            = Private.ExecEnv.conditionHelpers[uid].customTestFunctions or {}
          tinsert(Private.ExecEnv.conditionHelpers[uid].customTestFunctions, customCheck);
          local testFunctionNumber = #(Private.ExecEnv.conditionHelpers[uid].customTestFunctions);

          check = string.format("state and Private.ExecEnv.CallCustomConditionTest(%q, %s, state)",
                                uid, testFunctionNumber, trigger);
        end
      end
    elseif (cType == "bool" and value) then
      local rightSide = value == 0 and "false" or "true";
      check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]", variable)
              .. "==" .. rightSide
    elseif (cType == "string" and value) then
      if(op == "==") then
        check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]", variable)
                .. " == [[" .. value .. "]]";
      elseif (op  == "find('%s')") then
        check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]", variable)
                .. ":find([[" .. value .. "]], 1, true)";
      elseif (op == "match('%s')") then
        check = stateCheck .. stateVariableCheck .. "state[" .. trigger .. "]" .. string.format("[%q]",  variable)
                .. ":match([[" .. value .. "]], 1, true)";
      end
    end
    -- If adding a new condition type, don't forget to adjust the validator in the options code

    if (cType == "timer" and value) then
      if useModRate then
        recheckCode = "  nextTime = state[" .. trigger .. "] and not state[" .. trigger .. "].paused"
        .. " and state[" .. trigger .. "]" .. string.format("[%q]",  variable)
        .. " and (state[" .. trigger .. "]" .. string.format("[%q]",  variable) .. " - " .. value .. " * (state[" .. trigger .. "].modRate or 1.0))\n"
      else
        recheckCode = "  nextTime = state[" .. trigger .. "] and not state[" .. trigger .. "].paused"
        .. " and state[" .. trigger .. "]" .. string.format("[%q]",  variable)
        .. " and (state[" .. trigger .. "]" .. string.format("[%q]",  variable) .. " - " .. value .. ")\n"
      end
      recheckCode = recheckCode .. "  if (nextTime and (not recheckTime or nextTime < recheckTime) and nextTime >= now) then\n"
      recheckCode = recheckCode .. "    recheckTime = nextTime\n";
      recheckCode = recheckCode .. "  end\n"
    elseif (cType == "elapsedTimer" and value) then
      recheckCode = "  nextTime = state[" .. trigger .. "] and state[" .. trigger .. "]" .. string.format("[%q]",  variable) .. " and (state[" .. trigger .. "]" .. string.format("[%q]",  variable) .. " +" .. value .. ")\n";
      recheckCode = recheckCode .. "  if (nextTime and (not recheckTime or nextTime < recheckTime) and nextTime >= now) then\n"
      recheckCode = recheckCode .. "    recheckTime = nextTime\n";
      recheckCode = recheckCode .. "  end\n"
    end
  end

  return check, recheckCode;
end

local function CreateCheckCondition(uid, ret, condition, conditionNumber, allConditionsTemplate, nextIsLinked, debug)
  local usedStates = {};
  local check, recheckCode = CreateTestForCondition(uid, condition.check, allConditionsTemplate, usedStates);
  if not check then
    check = "false"
  end
  if condition.linked and conditionNumber > 1 then
    ret = ret .. "      elseif (" .. check .. ") then\n";
  else
    ret = ret .. "      if (" .. check .. ") then\n";
  end
  ret = ret .. "        newActiveConditions[" .. conditionNumber .. "] = true;\n";
  if not nextIsLinked then
    ret = ret .. "      end\n";
  end

  if (check) then
    ret = ret .. "\n";
  end
  return ret, recheckCode;
end

local function ParseProperty(property)
  local subIndex, prop = string.match(property, "^sub%.(%d*).(.*)")
  if subIndex then
    return tonumber(subIndex), prop
  else
    return nil, property
  end
end

local function GetBaseProperty(data, property, start)
  if (not data) then
    return nil;
  end

  local subIndex, prop = ParseProperty(property)
  if subIndex then
    return GetBaseProperty(data.subRegions[subIndex], prop, start)
  end

  start = start or 1;
  local next = string.find(property, ".", start, true);
  if (next) then
    return GetBaseProperty(data[string.sub(property, start, next - 1)], property, next + 1);
  end

  local key = string.sub(property, start);
  return data[key] or data[tonumber(key)];
end

local function CreateDeactivateCondition(ret, condition, conditionNumber, data, properties, usedProperties, debug)
  if (condition.changes) then
    ret = ret .. "  if (activatedConditions[".. conditionNumber .. "] and not newActiveConditions[" .. conditionNumber .. "]) then\n"
    if (debug) then ret = ret .. "    print('Deactivating condition " .. conditionNumber .. "' )\n"; end
    for changeNum, change in ipairs(condition.changes) do
      if (change.property) then
        local propertyData = properties and properties[change.property]
        if (propertyData and propertyData.type and propertyData.setter) then
          usedProperties[change.property] = true;
          ret = ret .. "    propertyChanges['" .. change.property .. "'] = " .. formatValueForAssignment(propertyData.type, GetBaseProperty(data, change.property)) .. "\n";
          if (debug) then ret = ret .. "    print('- " .. change.property .. " " ..formatValueForAssignment(propertyData.type,  GetBaseProperty(data, change.property)) .. "')\n"; end
        end
      end
    end
    ret = ret .. "  end\n"
  end

  return ret;
end

local function CreateActivateCondition(ret, id, condition, conditionNumber, properties, debug)
  if (condition.changes) then
    ret = ret .. "  if (newActiveConditions[" .. conditionNumber .. "]) then\n"
    ret = ret .. "    if (not activatedConditions[".. conditionNumber .. "]) then\n"
    if (debug) then ret = ret .. "      print('Activating condition " .. conditionNumber .. "' )\n"; end
    -- non active => active
    for changeNum, change in ipairs(condition.changes) do
      if (change.property) then
        local propertyData = properties and properties[change.property]
        if (propertyData and propertyData.type) then
          if (propertyData.setter) then
            ret = ret .. "      propertyChanges['" .. change.property .. "'] = "
                      .. formatValueForAssignment(propertyData.type, change.value) .. "\n"
            if (debug) then ret = ret .. "      print('- " .. change.property .. " "
                      .. formatValueForAssignment(propertyData.type, change.value) .. "')\n" end
          elseif (propertyData.action) then
            local pathToCustomFunction = "nil";
            local pathToFormatter = "nil"
            if (Private.ExecEnv.customConditionsFunctions[id]
              and Private.ExecEnv.customConditionsFunctions[id][conditionNumber]
              and  Private.ExecEnv.customConditionsFunctions[id][conditionNumber].changes
              and Private.ExecEnv.customConditionsFunctions[id][conditionNumber].changes[changeNum]) then
              pathToCustomFunction = string.format("Private.ExecEnv.customConditionsFunctions[%q][%s].changes[%s]",
                                                   id, conditionNumber, changeNum);
            end
            if Private.ExecEnv.conditionTextFormatters[id]
              and Private.ExecEnv.conditionTextFormatters[id][conditionNumber]
              and Private.ExecEnv.conditionTextFormatters[id][conditionNumber].changes
              and Private.ExecEnv.conditionTextFormatters[id][conditionNumber].changes[changeNum] then
              pathToFormatter = string.format("Private.ExecEnv.conditionTextFormatters[%q][%s].changes[%s]",
                                              id, conditionNumber, changeNum);
            end
            ret = ret .. "     region:" .. propertyData.action .. "("
                         .. formatValueForAssignment(propertyData.type, change.value, pathToCustomFunction, pathToFormatter) .. ")" .. "\n";
            if (debug) then ret = ret .. "     print('# " .. propertyData.action .. "("
                         .. formatValueForAssignment(propertyData.type, change.value, pathToCustomFunction, pathToFormatter) .. "')\n"; end
          end
        end
      end
    end
    ret = ret .. "    else\n"
    -- active => active, only override properties
    for changeNum, change in ipairs(condition.changes) do
      if (change.property) then
        local propertyData = properties and properties[change.property]
        if (propertyData and propertyData.type and propertyData.setter) then
          ret = ret .. "      if(propertyChanges['" .. change.property .. "'] ~= nil) then\n"
          ret = ret .. "        propertyChanges['" .. change.property .. "'] = "
                       .. formatValueForAssignment(propertyData.type, change.value) .. "\n"
          if (debug) then ret = ret .. "        print('- " .. change.property .. " "
                       .. formatValueForAssignment(propertyData.type,  change.value) .. "')\n" end
          ret = ret .. "      end\n"
        end
      end
    end
    ret = ret .. "    end\n"
    ret = ret .. "  end\n"
    ret = ret .. "\n";
    ret = ret .. "  activatedConditions[".. conditionNumber .. "] = newActiveConditions[" .. conditionNumber .. "]\n";
  end

  return ret;
end

function Private.GetSubRegionProperties(data, properties)
  if data.subRegions then
    local subIndex = {}
    for index, subRegion in ipairs(data.subRegions) do
      local subRegionTypeData = Private.subRegionTypes[subRegion.type];
      local propertiesFunction = subRegionTypeData and subRegionTypeData.properties
      local subProperties;
      if (type(propertiesFunction) == "function") then
        subProperties = propertiesFunction(data, subRegion);
      elseif propertiesFunction then
        subProperties = CopyTable(propertiesFunction)
      end

      if subProperties then
        for key, property in pairs(subProperties) do
          subIndex[key] = subIndex[key] and subIndex[key] + 1 or 1
          property.display = { subRegionTypeData.displayName .. " " .. subIndex[key],
                               property.display,
                               property.defaultProperty }
          properties["sub." .. index .. "." .. key ] = property;
        end
      end
    end
  end
end

function Private.GetProperties(data)
  local properties;
  local propertiesFunction = Private.regionTypes[data.regionType] and Private.regionTypes[data.regionType].properties;
  if (type(propertiesFunction) == "function") then
    properties = propertiesFunction(data);
  elseif propertiesFunction then
    properties = CopyTable(propertiesFunction);
  else
    properties = {}
  end

  Private.GetSubRegionProperties(data, properties)
  return properties;
end

function Private.LoadConditionPropertyFunctions(data)
  local id = data.id;
  if (data.conditions) then
    Private.ExecEnv.customConditionsFunctions[id] = {};
    for conditionNumber, condition in ipairs(data.conditions) do
      if (condition.changes) then
        for changeIndex, change in ipairs(condition.changes) do
          if ( (change.property == "chat" or change.property == "customcode") and type(change.value) == "table" and change.value.custom) then
            local custom = change.value.custom;
            local prefix, suffix;
            if (change.property == "chat") then
              prefix, suffix = "return ", "";
            else
              prefix, suffix = "return function()", "\nend";
            end
            local customFunc = WeakAuras.LoadFunction(prefix .. custom .. suffix);
            if (customFunc) then
              Private.ExecEnv.customConditionsFunctions[id][conditionNumber] = Private.ExecEnv.customConditionsFunctions[id][conditionNumber] or {};
              Private.ExecEnv.customConditionsFunctions[id][conditionNumber].changes = Private.ExecEnv.customConditionsFunctions[id][conditionNumber].changes or {};
              Private.ExecEnv.customConditionsFunctions[id][conditionNumber].changes[changeIndex] = customFunc;
            end
          end
          if change.property == "chat" then
            local getter = function(key, default)
              local fullKey = "message_format_" .. key
              if change.value[fullKey] == nil then
                change.value[fullKey] = default
              end
              return change.value[fullKey]
            end
            local formatters = change.value and Private.CreateFormatters(change.value.message, getter, true)
            Private.ExecEnv.conditionTextFormatters[id] = Private.ExecEnv.conditionTextFormatters[id] or {}
            Private.ExecEnv.conditionTextFormatters[id][conditionNumber] = Private.ExecEnv.conditionTextFormatters[id][conditionNumber] or {};
            Private.ExecEnv.conditionTextFormatters[id][conditionNumber].changes = Private.ExecEnv.conditionTextFormatters[id][conditionNumber].changes or {};
            Private.ExecEnv.conditionTextFormatters[id][conditionNumber].changes[changeIndex] = formatters;
          end
        end
      end
    end
  end
end

local globalConditions =
{
  ["incombat"] = {
    display = L["In Combat"],
    type = "bool",
    events = {"PLAYER_REGEN_ENABLED", "PLAYER_REGEN_DISABLED"},
    globalStateUpdate = function(state)
      state.incombat = UnitAffectingCombat("player");
    end
  },
  ["hastarget"] = {
    display = L["Has Target"],
    type = "bool",
    events = {"PLAYER_TARGET_CHANGED", "PLAYER_ENTERING_WORLD"},
    globalStateUpdate = function(state)
      state.hastarget = UnitExists("target");
    end
  },
  ["rangecheck"] = {
    display = WeakAuras.newFeatureString .. L["Range Check"],
    type = "range",
    control = "WeakAurasSpinBox",
    events = {"WA_SPELL_RANGECHECK"}
  },
  ["attackabletarget"] = {
    display = L["Attackable Target"],
    type = "bool",
    events = {"PLAYER_TARGET_CHANGED", "UNIT_FACTION"},
    globalStateUpdate = function(state)
      state.attackabletarget = UnitCanAttack("player", "target");
    end
  },
  ["customcheck"] = {
    display = L["Custom Check"],
    type = "customcheck"
  },
  ["alwaystrue"] = {
    display = L["Always True"],
    type = "alwaystrue"
  }
}

function Private.GetGlobalConditions()
  return globalConditions;
end

local function ConstructConditionFunction(data)
  local debug = false;
  if (not data.conditions or #data.conditions == 0) then
    return nil;
  end

  local usedProperties = {};

  local allConditionsTemplate = Private.GetTriggerConditions(data);
  allConditionsTemplate[-1] = Private.GetGlobalConditions();

  local ret = "";
  ret = ret .. "local newActiveConditions = {};\n"
  ret = ret .. "local propertyChanges = {};\n"
  ret = ret .. "local nextTime;\n"
  ret = ret .. string.format("local uid = %q\n", data.uid)
  ret = ret .. "return function(region, hideRegion)\n";
  if (debug) then ret = ret .. "  print('check conditions for:', region.id, region.cloneId)\n"; end
  ret = ret .. "  local id = region.id\n";
  ret = ret .. "  local cloneId = region.cloneId or ''\n";
  ret = ret .. "  local state = region.states\n"
  ret = ret .. "  local activatedConditions = WeakAuras.GetActiveConditions(id, cloneId)\n";
  ret = ret .. "  wipe(newActiveConditions)\n";
  ret = ret .. "  local recheckTime;\n"
  ret = ret .. "  local now = GetTime();\n"

  -- First Loop gather which conditions are active
  ret = ret .. "  if (not hideRegion) then\n"
  local recheckCode = ""
  if (data.conditions) then
    Private.ExecEnv.conditionHelpers[data.uid] = nil
    for conditionNumber, condition in ipairs(data.conditions) do
      local nextIsLinked = data.conditions[conditionNumber + 1] and data.conditions[conditionNumber + 1].linked
      local additionalRecheckCode
      ret, additionalRecheckCode = CreateCheckCondition(data.uid, ret, condition, conditionNumber, allConditionsTemplate, nextIsLinked, debug)
      if additionalRecheckCode then
        recheckCode = recheckCode .. "\n" .. additionalRecheckCode
      end
    end
  end
  ret = ret .. recheckCode
  ret = ret .. "  end\n";

  ret = ret .. "  if (recheckTime) then\n"
  ret = ret .. "    Private.ExecEnv.ScheduleConditionCheck(recheckTime, uid, cloneId);\n"
  ret = ret .. "  else\n"
  ret = ret .. "    Private.ExecEnv.CancelConditionCheck(uid, cloneId)"
  ret = ret .. "  end\n"

  local properties = Private.GetProperties(data);

  -- Now build a property + change list
  -- Second Loop deals with conditions that are no longer active
  ret = ret .. "  wipe(propertyChanges)\n"
  if (data.conditions) then
    for conditionNumber, condition in ipairs(data.conditions) do
      ret = CreateDeactivateCondition(ret, condition, conditionNumber, data, properties, usedProperties, debug)
    end
  end
  ret = ret .. "\n";

  -- Third Loop deals with conditions that are newly active
  if (data.conditions) then
    for conditionNumber, condition in ipairs(data.conditions) do
      ret = CreateActivateCondition(ret, data.id, condition, conditionNumber, properties, debug)
    end
  end

  -- Last apply changes to region
  for property, _  in pairs(usedProperties) do
    ret = ret .. "  if(propertyChanges['" .. property .. "'] ~= nil) then\n"
    local arg1 = "";
    if (properties[property].arg1) then
      if (type(properties[property].arg1) == "number") then
        arg1 = tostring(properties[property].arg1) .. ", ";
      else
        arg1 = "'" .. properties[property].arg1 .. "', ";
      end
    end

    local base = "region:"
    local subIndex = ParseProperty(property)
    if subIndex then
      base = "region.subRegions[" .. subIndex .. "]:"
    end

    ret = ret .. "    " .. base .. properties[property].setter .. "(" .. arg1 .. formatValueForCall(properties[property].type, property)  .. ")\n";
    if (debug) then ret = ret .. "    print('Calling "  .. properties[property].setter ..  " with', " .. arg1 ..  formatValueForCall(properties[property].type, property) .. ")\n"; end
    ret = ret .. "  end\n";
  end
  ret = ret .. "end\n";

  return ret;
end

local function CancelTimers(uid)
  conditionChecksTimers.recheckTime[uid] = nil;
  if (conditionChecksTimers.recheckHandle[uid]) then
    for _, v in pairs(conditionChecksTimers.recheckHandle[uid]) do
      timer:CancelTimer(v);
    end
  end
  conditionChecksTimers.recheckHandle[uid] = nil;
end

function Private.LoadConditionFunction(data)
  CancelTimers(data.uid)

  local checkConditionsFuncStr = ConstructConditionFunction(data);
  local checkConditionsFunc = checkConditionsFuncStr and Private.LoadFunction(checkConditionsFuncStr)

  checkConditions[data.uid] = checkConditionsFunc;
end

function Private.RunConditions(region, uid, hideRegion)
  if (checkConditions[uid]) then
    Private.ActivateAuraEnvironmentForRegion(region)
    checkConditions[uid](region, hideRegion);
    Private.ActivateAuraEnvironment()
  end
end


local dynamicConditionsFrame = nil;

local globalConditionAllState = {
  [""] = {
    show = true;
  }
};

local globalConditionState = globalConditionAllState[""];

function Private.GetGlobalConditionState()
  return globalConditionAllState;
end

local function runDynamicConditionFunctions(funcs)
  for uid in pairs(funcs) do
    local id = Private.UIDtoID(uid)
    Private.StartProfileAura(id)
    if (Private.IsAuraActive(uid) and checkConditions[uid]) then
      local activeStates = WeakAuras.GetActiveStates(id)
      for cloneId, state in pairs(activeStates) do
        local region = WeakAuras.GetRegion(id, cloneId)
        Private.ActivateAuraEnvironmentForRegion(region)
        checkConditions[uid](region, false)
        Private.ActivateAuraEnvironment()
      end
    end
    Private.StopProfileAura(id)
  end
end

local function UpdateDynamicConditionsStates(self, event)
  if (globalDynamicConditionFuncs[event]) then
    for i, func in ipairs(globalDynamicConditionFuncs[event]) do
      func(globalConditionState);
    end
  end
end

local function handleDynamicConditions(self, event)
  Private.StartProfileSystem("dynamic conditions")
  UpdateDynamicConditionsStates(self, event)
  if (dynamicConditions[event]) then
    runDynamicConditionFunctions(dynamicConditions[event]);
  end
  Private.StopProfileSystem("dynamic conditions")
end

local function UpdateDynamicConditionsPerUnitState(self, event, unit)
  if unit then
    local unitEvent = event..":"..unit
    if globalDynamicConditionFuncs[unitEvent] then
      for i, func in ipairs(globalDynamicConditionFuncs[unitEvent]) do
        func(globalConditionState);
      end
    end
  end
end

local function handleDynamicConditionsPerUnit(self, event, unit)
  Private.StartProfileSystem("dynamic conditions")
  if unit then
    local unitEvent = event..":"..unit
    UpdateDynamicConditionsPerUnitState(self, event, unit)
    if (dynamicConditions[unitEvent]) then
      runDynamicConditionFunctions(dynamicConditions[unitEvent]);
    end
  end
  Private.StopProfileSystem("dynamic conditions")
end

local lastDynamicConditionsUpdateCheck;
local function handleDynamicConditionsOnUpdate(self)
  handleDynamicConditions(self, "FRAME_UPDATE");
  if (not lastDynamicConditionsUpdateCheck or GetTime() - lastDynamicConditionsUpdateCheck > 0.2) then
    lastDynamicConditionsUpdateCheck = GetTime();
    handleDynamicConditions(self, "WA_SPELL_RANGECHECK");
  end
end

local registeredGlobalFunctions = {};

local function EvaluateCheckForRegisterForGlobalConditions(uid, check, allConditionsTemplate, register)
  local trigger = check and check.trigger;
  local variable = check and check.variable;

  if (trigger == -2) then
    if (check.checks) then
      for _, subcheck in ipairs(check.checks) do
        EvaluateCheckForRegisterForGlobalConditions(uid, subcheck, allConditionsTemplate, register);
      end
    end
  elseif trigger == -1 and variable == "customcheck" then
    if check.op then
      for event in string.gmatch(check.op, "[%w_:]+") do
        if (not dynamicConditions[event]) then
          register[event] = true;
          dynamicConditions[event] = {};
        end
        dynamicConditions[event][uid] = true;
      end
    end
  elseif (trigger and variable) then
    local conditionTemplate = allConditionsTemplate[trigger] and allConditionsTemplate[trigger][variable];
    if (conditionTemplate and conditionTemplate.events) then
      for _, event in ipairs(conditionTemplate.events) do
        if (not dynamicConditions[event]) then
          register[event] = true;
          dynamicConditions[event] = {};
        end
        dynamicConditions[event][uid] = true;
      end

      if (conditionTemplate.globalStateUpdate and not registeredGlobalFunctions[variable]) then
        registeredGlobalFunctions[variable] = true;
        for _, event in ipairs(conditionTemplate.events) do
          globalDynamicConditionFuncs[event] = globalDynamicConditionFuncs[event] or {};
          tinsert(globalDynamicConditionFuncs[event], conditionTemplate.globalStateUpdate);
        end
        conditionTemplate.globalStateUpdate(globalConditionState);
      end
    end
  end
end

function Private.RegisterForGlobalConditions(uid)
  local data = Private.GetDataByUID(uid);
  for event, conditionFunctions in pairs(dynamicConditions) do
    conditionFunctions[uid] = nil;
  end

  local register = {};
  if (data.conditions) then
    local allConditionsTemplate = Private.GetTriggerConditions(data);
    allConditionsTemplate[-1] = Private.GetGlobalConditions();

    for conditionNumber, condition in ipairs(data.conditions) do
      EvaluateCheckForRegisterForGlobalConditions(uid, condition.check, allConditionsTemplate, register);
    end
  end

  if (next(register) and not dynamicConditionsFrame) then
    dynamicConditionsFrame = CreateFrame("Frame");
    dynamicConditionsFrame:SetScript("OnEvent", handleDynamicConditions);
    dynamicConditionsFrame.units = {}
    Private.frames["Rerun Conditions Frame"] = dynamicConditionsFrame
  end

  for event in pairs(register) do
    if (event == "FRAME_UPDATE" or event == "WA_SPELL_RANGECHECK") then
      if (not dynamicConditionsFrame.onUpdate) then
        dynamicConditionsFrame:SetScript("OnUpdate", handleDynamicConditionsOnUpdate);
        dynamicConditionsFrame.onUpdate = true;
      end
    else
      local unitEvent, unit = event:match("([^:]+):([^:]+)")
      if unitEvent and unit then
        unit = unit:lower()
        if not dynamicConditionsFrame.units[unit] then
          dynamicConditionsFrame.units[unit] = CreateFrame("Frame");
          dynamicConditionsFrame.units[unit]:SetScript("OnEvent", handleDynamicConditionsPerUnit);
        end
        pcall(dynamicConditionsFrame.units[unit].RegisterUnitEvent, dynamicConditionsFrame.units[unit], unitEvent, unit);
        UpdateDynamicConditionsPerUnitState(dynamicConditionsFrame, event, unit)
      else
        pcall(dynamicConditionsFrame.RegisterEvent, dynamicConditionsFrame, event);
        UpdateDynamicConditionsStates(dynamicConditionsFrame, event)
      end
    end
  end
end

function Private.UnregisterForGlobalConditions(uid)
  for event, condFuncs in pairs(dynamicConditions) do
    condFuncs[uid] = nil;
    if next(condFuncs) == nil then
      local unitEvent, unit = event:match("([^:]+):([^:]+)")
      if unitEvent and unit then
        unit = unit:lower()
        pcall(dynamicConditionsFrame.units[unit].UnregisterEvent, dynamicConditionsFrame.units[unit], unitEvent);
      elseif (event == "FRAME_UPDATE" or event == "WA_SPELL_RANGECHECK") then
        if (event == "FRAME_UPDATE" and dynamicConditions["WA_SPELL_RANGECHECK"] == nil)
        or (event == "WA_SPELL_RANGECHECK" and dynamicConditions["FRAME_UPDATE"] == nil)
        then
          dynamicConditionsFrame:SetScript("OnUpdate", nil)
          dynamicConditionsFrame.onUpdate = false
        end
      else
        pcall(dynamicConditionsFrame.UnregisterEvent, dynamicConditionsFrame, event);
      end
      dynamicConditions[event] = nil
    end
  end
end

function Private.UnloadAllConditions()
  for uid in pairs(conditionChecksTimers.recheckTime) do
    if (conditionChecksTimers.recheckHandle[uid]) then
      for _, v in pairs(conditionChecksTimers.recheckHandle[uid]) do
        timer:CancelTimer(v)
      end
    end
  end
  wipe(conditionChecksTimers.recheckTime)
  wipe(conditionChecksTimers.recheckHandle)

  dynamicConditions = {}
  if dynamicConditionsFrame then
    dynamicConditionsFrame:UnregisterAllEvents()
    for _, frame in pairs(dynamicConditionsFrame.units) do
      frame:UnregisterAllEvents()
    end
    dynamicConditionsFrame:SetScript("OnUpdate", nil)
    dynamicConditionsFrame.onUpdate = false
  end
end

function Private.UnloadConditions(uid)
  CancelTimers(uid)
  Private.UnregisterForGlobalConditions(uid);
end
