
----------------------------------------------------------------------------
-- Object property parsing helper functions (from Tiled custom property list)

local function is_valid_string(str)
  return (str ~= nil and string.len(str) > 0)
end

local function get_readable_object_name(obj)
  if obj.name == nil then
    return "<unnamed>"
  else
    return obj.name
  end
end

local function assert_valid_property_value(val, obj, property_id, value_as_str)
  if val == nil then
    -- Assert inside if so that we don't concatenate the whole string and fill garbage all the time
    assert(false, "Invalid property value '" .. value_as_str .. "' for property '" .. property_id .. "' in object '" .. get_readable_object_name(obj) .. "'.")
  end
end

local function get_obj_property(obj, property_id)
  assert(obj ~= nil)
  local val = obj.properties[property_id]
  if not is_valid_string(val) then
    assert(false, "Missing property: '" .. property_id .. "' in object '" .. get_readable_object_name(obj) .. "'")
  end
  return val
end

-- Object property exists and value is a valid non-zero string
local function has_obj_property(obj, property_id)
  return is_valid_string(obj.properties[property_id])
end

local function get_obj_property_as_number(obj, property_id)
  local as_str = get_obj_property(obj, property_id)
  local res = tonumber(as_str)
  assert_valid_property_value(res, obj, property_id, as_str)
  return res
end

local function get_obj_property_as_enum_defaulted(obj, property_id, cpp_enum_prefix, default_value)
  local res = default_value
  if has_obj_property(obj, property_id) then
    local as_str = get_obj_property(obj, property_id)
    res = _G[cpp_enum_prefix .. as_str]
    assert_valid_property_value(res, obj, property_id, as_str)
  end
  return res
end

local function get_obj_property_as_enum(obj, property_id, cpp_enum_prefix)
  return get_obj_property_as_enum_defaulted(obj, property_id, cpp_enum_prefix, nil)
end

----------------------------------------------------------------------------
-- Parse game custom elements from the game objects / areas / etc.

local function to_game_coord(map, vec_tiled)
  return vec(vec_tiled.x * map.coordinate_scale, vec_tiled.y * map.coordinate_scale)
end

local function parse_rect(map, obj)
  assert(obj ~= nil)
  assert(obj.shape == "rectangle")

  local size = to_game_coord(map, vec(obj.width, obj.height))
  assert(size.x >= 0 and size.y >= 0)
  assert(obj.rotation == 0)

  local pos = to_game_coord(map, obj)
  return {
    min = pos,
    max = vec_add(pos, size),
  }
end

local function parse_object_position(map, obj)
  local rect = parse_rect(map, obj)
  local center = vec_mul_scalar(vec_add(rect.min, rect.max), 0.5)
  
  -- Tiled objects are placed by left,BOTTOM position for some reason, so flip the Y
  local y_size = rect.max.y - rect.min.y
  center.y = center.y - y_size

  return center
end

-- Object parser functions
local OBJ_PARSE = {}

function OBJ_PARSE.t_group_spawn_area(map, object_layer, object_data)
  local res = {
    area            = parse_rect(map, object_data),
    spawn_group_id  = get_obj_property_as_enum(object_data, "contents", "CPP_"),
  }

  table_insert_forced(object_layer, "group_spawn_area", res)
end

function OBJ_PARSE.t_named_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "named_area", {
    area = parse_rect(map, object_data),
    name = object_data.name,
  })
end

function OBJ_PARSE.t_enemy_spawn_generic(map, object_layer, object_data)
  table_insert_forced(object_layer, "enemy_spawn_generic", {
    area = parse_rect(map, object_data),
  })
end

function OBJ_PARSE.t_player_spawn_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "player_spawn_area", {
    area = parse_rect(map, object_data),
  })
end

function OBJ_PARSE.t_fixed_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "fixed_area", {
    area = parse_rect(map, object_data),
  })
end

function OBJ_PARSE.t_wall_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "wall_area", 
  {
    area = parse_rect(map, object_data),
    wall_entity_id = get_obj_property_as_enum_defaulted(object_data, "entity_type", "CPP_", CPP_et_NONE),
  })
end

function OBJ_PARSE.t_gate_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "gate_area", 
  {
    area = parse_rect(map, object_data),
    wall_entity_id = get_obj_property_as_enum_defaulted(object_data, "entity_type", "CPP_", CPP_et_NONE),
  })
end

function OBJ_PARSE.t_lake_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "lake_area", {
    area = parse_rect(map, object_data),
  })
end

function OBJ_PARSE.t_army_area(map, object_layer, object_data)
  table_insert_forced(object_layer, "army_area", {
    area = parse_rect(map, object_data),
  })
end

local function prepare_object_layer(map, object_layer)
  assert(object_layer.type == "objectgroup")

  -- Handle all objectgroup objects by their type
  for _,obj in ipairs(object_layer.objects) do
    
    if obj.gid ~= nil then
      -- Individual entity/object placement

      -- Find object type
      local function find_object_type() 
        local set_index = obj.gid - map.tilesets[2].firstgid
        for _,t in pairs(map.tilesets[2].tiles) do
          if t.id == set_index then
            return t
          end
        end
        assert(false, "Error: Could not find info for object " .. obj.gid .." in object info. " .. obj.name)
        return nil
      end

      local obj_tile_info = find_object_type()
      local obj_pos = parse_object_position(map, obj)

      -- Store the spawn data
      local entity_spawn = {
        name      = obj.name,
        pos       = obj_pos,
        entity_id = get_obj_property_as_enum(obj_tile_info, "object_type", "CPP_"),
      }
      table_insert_forced(object_layer, "entity_spawn", entity_spawn)

    else
      -- Geometry objects (spawn area rectangles, etc.)
      local parser = OBJ_PARSE[obj.type]
      assert(parser ~= nil, "Unknown object type " .. obj.type .. ". Failing.")
      parser(map, object_layer, obj)
    end
  end

end

----------------------------------------------------------------------------
-- Game layer randomization system

-- 'Constants' for 
local RANDOM_MODE_unspecified = 0
local RANDOM_MODE_force_one = 1

local function parse_randomization_parameters(map)

  local function get_matching_properties(match_str)
    local res = {}
    for key,p in table_ordered_pairs(map.properties) do
      local capture = string.match(key, match_str)
      if capture ~= nil then
        res[key] = p
      end
    end
    return res
  end

  -- Note: iterate random set properties with table_ordered_pairs to stay deterministic

  -- Parse the randomization sets from map properties
  local props_rand_set = get_matching_properties("random_set_")
  local random_sets = {}

  local random_mode = RANDOM_MODE_unspecified
  if table_size(props_rand_set) > 0 then
      random_mode = RANDOM_MODE_force_one
  end

  for key,p in table_ordered_pairs(props_rand_set) do
    assert(random_mode ~= RANDOM_MODE_unspecified, "Randomization mode not specified, but random sets are in level.")

    local index = tonumber(string.match(key, "random_set_(%d+)"))
    assert(index ~= nil, "Unknown property: " .. map.map_asset_id .. "," .. key)
    assert(index > 0, "Failed to parse property: " .. key)

    local ids = string_split(p, ",")
    assert(#ids >= 2)
    local prob_value_str = table.remove(ids)
    local prob_value = tonumber(prob_value_str)
    assert(prob_value ~= nil)

    table.insert(random_sets, 
    {
      index       = index,
      layer_ids   = ids,
      prob_value  = prob_value,
    })
  end

  -- If force_one randomization, normalize probabilities
  if random_mode == RANDOM_MODE_force_one then

    -- Calculate sum of specified probability numbers
    local total_prob = 0
    for _,s in pairs(random_sets) do
      total_prob = total_prob + s.prob_value
    end
    assert(total_prob > 0)

    -- Normalize so sum is 1.0
    for _,s in pairs(random_sets) do
      s.prob_value = s.prob_value / total_prob
    end
  end

  return {
    random_mode = random_mode,
    random_sets = random_sets,
  }
end

-- Fetch layers from map and filter them according to randomization parameters
local function do_map_randomization(rand_params, map)
  assert(map.properties ~= nil)

  local function layer_exists(layer_name)
    for _,l in ipairs(map.layers) do
      if l.name  == layer_name then
        return true
      end
    end
    return false
  end

  -- Perform the actual layer set random selection
  local selected_set = {}
  local all_optional_layers = {}

  local function flag_random_set_layers(layer_ids, is_selected)
    for _,layer_id in pairs(layer_ids) do
      -- Flag all layers that are specified in SOME randomization set, even if not selected
      all_optional_layers[layer_id] = true

      -- If this randomization set was selected, flag the layers it refers to
      if is_selected then
        assert(layer_exists(layer_id), "Given in randomization set layer doesn't exist in level. (" .. map.map_asset_id .. ", " .. layer_id .. ")")
        selected_set[layer_id] = true
      end
    end
  end

  -- Perform randomization
 if rand_params.random_mode == RANDOM_MODE_force_one then
    local total_probability = 0.0
    local selection_done = false
    local exclusive_mode_random_value = random_game_float()
    assert(exclusive_mode_random_value >= 0 and exclusive_mode_random_value <= 1)

    for idx,random_set in ipairs(rand_params.random_sets) do

      local is_selected = false

      -- Pick only one of the sets specified
      total_probability = total_probability + random_set.prob_value
      assert(total_probability <= 1.001, "Total probability over 1: " .. map.map_asset_id .. ": " .. total_probability)
      is_selected = (not selection_done) and (exclusive_mode_random_value <= total_probability)
      selection_done = selection_done or is_selected

      -- Force one to be selected if not selected before (might not because of floating point inaccuracies (maybe))
      if idx == #rand_params.random_sets and not selection_done then
        assert(#rand_params.random_sets > 0)
        is_selected = true
      end

      flag_random_set_layers(random_set.layer_ids, is_selected)
    end

    -- Assert that something was selected
    assert(table_size(selected_set) > 0, "force_one randomization mode failed (selected .. " .. table_size(selected_set) .. " layers.")
  else
    -- No randomization in this map
    assert(rand_params.random_mode == RANDOM_MODE_unspecified)
  end

  -- Generate new layers table from the random selection
  -- Note: need to keep original order in result, so for example tile layers don't swap rendering order!
  local selected_layers = {}

  for _,layer in pairs(map.layers) do
    if all_optional_layers[layer.name] == nil then
      -- A layer that was not specified in any randomization set (always insert)
      table.insert(selected_layers, layer)
    elseif selected_set[layer.name] then
      -- A layer that was selected with random
      table.insert(selected_layers, layer)
    end
  end

  -- Overwrite the old layers with new layer table
  map.layers = selected_layers

end

----------------------------------------------------------------------------

-- Make sure Tiled authored map has everything we assume it has and do some preprocessing
local function prepare_and_verify_tiled_map(map_asset_id, map)
  assert(map_asset_id ~= nil)
  assert(map ~= nil, "Failed to load map '" .. map_asset_id .. "'")
  assert(#map.layers > 0)

  -- Assert tilesets are as expected
  assert(#map.tilesets == 2)
  assert(map.tilesets[1].name == "tiles_default")
  assert(map.tilesets[1].firstgid == 1)
  assert(map.tilesets[2].name == "objects")
  assert(map.tilesets[2].firstgid == 513, "Assumed 512 tiles in tile map, has this changed") -- Consistency assert, change value as needed

  -- Calculate coordinate scale (Tiled -> game)
  local coordinate_scale = 1.0 / map.tilesets[1].tilewidth;
  assert(map.tilesets[1].tilewidth == 62.0, "Tiling resolution changed maybe, why?") -- Just sanity verify
  map.coordinate_scale = coordinate_scale

  -- Do randomization
  local rand_params = parse_randomization_parameters(map)
  do_map_randomization(rand_params, map)

  -- Parse object data
  for _,l in ipairs(map.layers) do
    if l.type  == "objectgroup" then
      prepare_object_layer(map, l)
    end
  end
end

----------------------------------------------------------------------------
-- Main generator function, called by C++ side

set_global("load_tiled_level", function(tiled_map_asset_id)

  -- Load the raw Tiled export  
  local tiled_map = run_external_script(tiled_map_asset_id)
  tiled_map.map_asset_id = tiled_map_asset_id

  -- Convert etc. tiled data to the format the game expects them to be in
  prepare_and_verify_tiled_map(tiled_map_asset_id, tiled_map)

  return { tiled_map = tiled_map, }
end)
