--[[ Copyright (c) 2017 Optera
 * Part of Logistics Train Network
 *
 * See LICENSE.md in the project directory for license information.
--]]


-- update stop output when train enters stop
function TrainArrives(train)
  local stopID = train.station.unit_number
  local stop = global.LogisticTrainStops[stopID]
  if stop then
    local stop_name = stop.entity.backer_name
    -- assign main loco name and force
    local loco = Get_Main_Locomotive(train)
    local trainForce = nil
    local trainName = nil
    if loco then
      trainName = loco.backer_name
      trainForce = loco.force
    end

    -- add train to global.StoppedTrains
    global.StoppedTrains[train.id] = {
      train = train,
      name = trainName,
      force = trainForce,
      stopID = stopID,
    }

    -- add train to global.LogisticTrainStops
    stop.parked_train = train
    stop.parked_train_id = train.id

    local frontDistance = Get_Distance(train.front_stock.position, train.station.position)
    local backDistance = Get_Distance(train.back_stock.position, train.station.position)
    if frontDistance > backDistance then
      stop.parked_train_faces_stop = false
    else
      stop.parked_train_faces_stop = true
    end
    local is_provider = false

    -- if message_level >= 3 then printmsg({"ltn-message.train-arrived", tostring(trainName), stop_name}, trainForce, false) end
    if message_level >= 3 then printmsg({"ltn-message.train-arrived", Make_Train_RichText(train, nil), format("[train-stop=%d]", stopID)}, trainForce, false) end
    if debug_log then log(format("(TrainArrives) Train [%d] \"%s\": arrived at LTN-stop [%d] \"%s\"; train_faces_stop: %s", train.id, trainName, stopID, stop_name, stop.parked_train_faces_stop )) end

    if stop.error_code == 0 then
      if stop.is_depot then
        local delivery = global.Dispatcher.Deliveries[train.id]
        if delivery then
          -- delivery should have been removed when leaving requester. Handle like delivery timeout.
          local from_entity = global.LogisticTrainStops[delivery.from_id] and global.LogisticTrainStops[delivery.from_id].entity
          local to_entity = global.LogisticTrainStops[delivery.to_id] and global.LogisticTrainStops[delivery.to_id].entity
          if message_level >= 1 then
            printmsg({
              "ltn-message.delivery-removed-depot",
              Make_Stop_RichText(from_entity) or delivery.from,
              Make_Stop_RichText(to_entity) or delivery.to
            }, delivery.force, false)
          end
          if debug_log then log(format("(TrainArrives) Train [%d] \"%s\": Entered Depot with active Delivery. Failing Delivery and reseting train.", train.id, trainName)) end
          script.raise_event(on_delivery_failed_event, {train_id = train.id, shipment = delivery.shipment})
          RemoveDelivery(train.id)
        end

        -- clean fluid residue
        local train_items = train.get_contents()
        local train_fluids = train.get_fluid_contents()
        if table_size(train_fluids) > 0 and depot_fluid_cleaning > 0 then
          -- cleaning per wagon
          for i, wagon in pairs(train.fluid_wagons) do
            for fluid, count in pairs(wagon.get_fluid_contents()) do
              if count <= depot_fluid_cleaning then
                local removed = wagon.remove_fluid({name=fluid, amount=count})
                if debug_log then log(format("(TrainArrives) Train \"%s\"[%d]: Depot fluid removal %s %f/%f", trainName, i, fluid, removed, count)) end
              end
            end
          end
          -- cleaning whole train doesn't work in 1.1.26
          -- for fluid, count in pairs(train_fluids) do
          --   if count <= depot_fluid_cleaning then
          --     local removed = train.remove_fluid({name=fluid, amount=count})
          --     log(format("Train %s: removed %s %f/%f", trainName, fluid, removed, count))
          --   end
          -- end
          train_fluids = train.get_fluid_contents()
        end

        -- check for leftover cargo
        if table_size(train_items) > 0 then
          create_alert(stop.entity, "cargo-warning", {"ltn-message.depot_left_over_cargo", trainName, stop_name}, trainForce)
        end
        if table_size(train_fluids) > 0 then
          create_alert(stop.entity, "cargo-warning", {"ltn-message.depot_left_over_cargo", trainName, stop_name}, trainForce)
        end

        -- make train available for new deliveries
        local capacity, fluid_capacity = GetTrainCapacity(train)
        global.Dispatcher.availableTrains[train.id] = {
          train = train,
          surface = loco.surface,
          force = trainForce,
          depot_priority = stop.depot_priority,
          network_id = stop.network_id,
          capacity = capacity,
          fluid_capacity = fluid_capacity
        }
        global.Dispatcher.availableTrains_total_capacity = global.Dispatcher.availableTrains_total_capacity + capacity
        global.Dispatcher.availableTrains_total_fluid_capacity = global.Dispatcher.availableTrains_total_fluid_capacity + fluid_capacity
        -- log("added available train "..train.id..", inventory: "..tostring(global.Dispatcher.availableTrains[train.id].capacity)..", fluid capacity: "..tostring(global.Dispatcher.availableTrains[train.id].fluid_capacity))

        -- reset schedule
        local schedule = {current = 1, records = {}}
        schedule.records[1] = NewScheduleRecord(stop_name, "inactivity", depot_inactivity)
        train.schedule = schedule

        -- reset filters and bars
        if depot_reset_filters and train.cargo_wagons then
          for n,wagon in pairs(train.cargo_wagons) do
            local inventory = wagon.get_inventory(defines.inventory.cargo_wagon)
            if inventory then
              if inventory.is_filtered() then
                -- log("Cargo-Wagon["..tostring(n).."]: reseting "..tostring(#inventory).." filtered slots.")
                for slotIndex=1, #inventory, 1 do
                  inventory.set_filter(slotIndex, nil)
                end
              end
              if inventory.supports_bar and #inventory - inventory.get_bar() > 0 then
                -- log("Cargo-Wagon["..tostring(n).."]: reseting "..tostring(#inventory - inventory.get_bar()).." locked slots.")
                inventory.set_bar()
              end
            end
          end
        end

        setLamp(stop, "blue", 1)

      else -- stop is no Depot
        -- check requester for incorrect shipment
        local delivery = global.Dispatcher.Deliveries[train.id]
        if delivery then
          is_provider = delivery.from_id == stop.entity.unit_number
          if delivery.to_id == stop.entity.unit_number then
            local requester_unscheduled_cargo = false
            local unscheduled_load = {}
            local train_items = train.get_contents()
            for name, count in pairs(train_items) do
              local typed_name = "item,"..name
              if not delivery.shipment[typed_name] then
                requester_unscheduled_cargo = true
                unscheduled_load[typed_name] = count
              end
            end
            local train_fluids = train.get_fluid_contents()
            for name, count in pairs(train_fluids) do
              local typed_name = "fluid,"..name
              if not delivery.shipment[typed_name] then
                requester_unscheduled_cargo = true
                unscheduled_load[typed_name] = count
              end
            end
            if requester_unscheduled_cargo then
              create_alert(stop.entity, "cargo-alert", {"ltn-message.requester_unscheduled_cargo", trainName, stop_name}, trainForce)
              script.raise_event(on_requester_unscheduled_cargo_alert, {train = train, station = stop.entity, planned_shipment = delivery.shipment, unscheduled_load = unscheduled_load})
            end
          end
        end

        -- set lamp to blue for LTN controlled trains
        for i=1, #stop.active_deliveries, 1 do
          if stop.active_deliveries[i] == train.id then
            setLamp(stop, "blue", #stop.active_deliveries)
            break
          end
        end
      end
    end

    UpdateStopOutput(stop, is_provider and not(provider_show_existing_cargo) )
  end
end

-- update stop output when train leaves stop
-- when called from on_train_created stoppedTrain.train will be invalid
function TrainLeaves(trainID)
  local stoppedTrain = global.StoppedTrains[trainID] -- checked before every call of TrainLeaves
  local train = stoppedTrain.train
  local stopID = stoppedTrain.stopID
  local stop = global.LogisticTrainStops[stopID]
  if not stop then
    if debug_log then log(format("(TrainLeaves) Error: StopID [%d] not found in global.LogisticTrainStops", stopID )) end
    global.StoppedTrains[trainID] = nil
    return
  end
  if not stop.entity.valid or not stop.input.valid or not stop.output.valid or not stop.lamp_control.valid then
    if debug_log then log(format("(TrainLeaves) Error: StopID [%d] contains invalid entity. Processing skipped, train inventory not updated.", stopID )) end
    global.StoppedTrains[trainID] = nil
    -- don't call RemoveStop here as RemoveStop calls TrainLeaves again
    return
  end
  local stop_name = stop.entity.backer_name

  -- train was stopped at LTN depot
  if stop.is_depot then
    if global.Dispatcher.availableTrains[trainID] then -- trains are normally removed when deliveries are created
      global.Dispatcher.availableTrains_total_capacity = global.Dispatcher.availableTrains_total_capacity - global.Dispatcher.availableTrains[trainID].capacity
      global.Dispatcher.availableTrains_total_fluid_capacity = global.Dispatcher.availableTrains_total_fluid_capacity - global.Dispatcher.availableTrains[trainID].fluid_capacity
      global.Dispatcher.availableTrains[trainID] = nil
    end
    if stop.error_code == 0 then
      setLamp(stop, "green", 1)
    end
    if debug_log then log(format("(TrainLeaves) Train [%d] \"%s\": left Depot [%d] \"%s\".", trainID, stoppedTrain.name, stopID, stop.entity.backer_name )) end

  -- train was stopped at LTN stop
  else
    -- remove delivery from stop
    for i=#stop.active_deliveries, 1, -1 do
      if stop.active_deliveries[i] == trainID then
        table.remove(stop.active_deliveries, i)
      end
    end

    local delivery = global.Dispatcher.Deliveries[trainID]
    if train.valid and delivery then
      if delivery.from_id == stop.entity.unit_number then
        -- update delivery counts to train inventory
        local actual_load = {}
        local unscheduled_load = {}
        local provider_unscheduled_cargo = false
        local provider_missing_cargo = false
        local train_items = train.get_contents()
        for name, count in pairs(train_items) do
          local typed_name = "item,"..name
          local planned_count = delivery.shipment[typed_name]
          if planned_count then
            actual_load[typed_name] = count -- update shipment to actual inventory
            if count < planned_count then
              -- underloaded
              provider_missing_cargo = true
            end
          else
            -- loaded wrong items
            unscheduled_load[typed_name] = count
            provider_unscheduled_cargo = true
          end
        end
        local train_fluids = train.get_fluid_contents()
        for name, count in pairs(train_fluids) do
          local typed_name = "fluid,"..name
          local planned_count = delivery.shipment[typed_name]
          if planned_count then
            actual_load[typed_name] = count -- update shipment actual inventory
            if planned_count-count > 0.1 then -- prevent lsb errors
              -- underloaded
              provider_missing_cargo = true
            end
          else
            -- loaded wrong fluids
            unscheduled_load[typed_name] = count
            provider_unscheduled_cargo = true
          end
        end
        delivery.pickupDone = true -- remove reservations from this delivery
        if debug_log then log(format("(TrainLeaves) Train [%d] \"%s\": left Provider [%d] \"%s\"; cargo: %s; unscheduled: %s ", trainID, stoppedTrain.name, stopID, stop.entity.backer_name, serpent.line(actual_load), serpent.line(unscheduled_load) )) end
        global.StoppedTrains[trainID] = nil

        if provider_missing_cargo then
          create_alert(stop.entity, "cargo-alert", {"ltn-message.provider_missing_cargo", stoppedTrain.name, stop_name}, stoppedTrain.force)
          script.raise_event(on_provider_missing_cargo_alert, {
            train = train,
            station = stop.entity,
            planned_shipment = delivery.shipment,
            actual_shipment = actual_load
          })
        end
        if provider_unscheduled_cargo then
          create_alert(stop.entity, "cargo-alert", {"ltn-message.provider_unscheduled_cargo", stoppedTrain.name, stop_name}, stoppedTrain.force)
          script.raise_event(on_provider_unscheduled_cargo_alert, {
            train = train,
            station = stop.entity,
            planned_shipment = delivery.shipment,
            unscheduled_load = unscheduled_load
          })
        end
        script.raise_event(on_delivery_pickup_complete_event, {
          train_id = trainID,
          train = train,
          planned_shipment = delivery.shipment,
          actual_shipment = actual_load
        })
        delivery.shipment = actual_load

      elseif delivery.to_id == stop.entity.unit_number then
        -- reset schedule before API events
        if requester_delivery_reset and train.schedule then
          local schedule = {current = 1, records = {}}
          schedule.records[1] = NewScheduleRecord(train.schedule.records[1].station, "inactivity", depot_inactivity)
          train.schedule = schedule
        end

        local remaining_load = {}
        local requester_left_over_cargo = false
        local train_items = train.get_contents()
        for name, count in pairs(train_items) do
          -- not fully unloaded
          local typed_name = "item,"..name
          requester_left_over_cargo = true
          remaining_load[typed_name] = count
        end
        local train_fluids = train.get_fluid_contents()
        for name, count in pairs(train_fluids) do
          -- not fully unloaded
          local typed_name = "fluid,"..name
          requester_left_over_cargo = true
          remaining_load[typed_name] = count
        end

        if debug_log then log(format("(TrainLeaves) Train [%d] \"%s\": left Requester [%d] \"%s\" with left over cargo: %s", trainID, stoppedTrain.name, stopID, stop.entity.backer_name, serpent.line(remaining_load))) end
        -- signal completed delivery and remove it
        if requester_left_over_cargo then
          create_alert(stop.entity, "cargo-alert", {"ltn-message.requester_left_over_cargo", stoppedTrain.name, stop_name}, stoppedTrain.force)
          script.raise_event(on_requester_remaining_cargo_alert, {
            train = train,
            station = stop.entity,
            remaining_load = remaining_load
          })
        end
        script.raise_event(on_delivery_completed_event, {
          train_id = trainID,
          train = train,
          shipment = delivery.shipment})
        RemoveDelivery(trainID)
      else
        if debug_log then log(format("(TrainLeaves) Train [%d] \"%s\": left LTN-stop [%d] \"%s\".", trainID, stoppedTrain.name, stopID, stop.entity.backer_name)) end
      end
    end
    if stop.error_code == 0 then
      if #stop.active_deliveries > 0 then
        setLamp(stop, "yellow", #stop.active_deliveries)
      else
        setLamp(stop, "green", 1)
      end
    end
  end

  -- remove train reference
  stop.parked_train = nil
  stop.parked_train_id = nil
  -- if message_level >= 3 then printmsg({"ltn-message.train-left", tostring(stoppedTrain.name), stop.entity.backer_name}, stoppedTrain.force) end
  if message_level >= 3 then printmsg({"ltn-message.train-left", Make_Train_RichText(train, stoppedTrain.name), format("[train-stop=%d]", stopID)}, stoppedTrain.force, false) end
  UpdateStopOutput(stop)

  global.StoppedTrains[trainID] = nil
end

-- local reverse_defines = require('__flib__.reverse-defines')

function OnTrainStateChanged(event)
  -- log(game.tick.." (OnTrainStateChanged) Train name: "..tostring(Get_Train_Name(event.train))..", train.id:"..tostring(event.train.id).." stop: "..tostring(event.train.station and event.train.station.backer_name)..", state: "..reverse_defines.train_state[event.old_state].." > "..reverse_defines.train_state[event.train.state] )
  local train = event.train
  if train.state == defines.train_state.wait_station and train.station ~= nil and ltn_stop_entity_names[train.station.name] then
    TrainArrives(train)
  elseif event.old_state == defines.train_state.wait_station and global.StoppedTrains[train.id] then -- update to 0.16
    TrainLeaves(train.id)
  end
end

-- updates or removes delivery references
function Update_Delivery(old_train_id, new_train)
  local delivery = global.Dispatcher.Deliveries[old_train_id]

  -- expanded RemoveDelivery(old_train_id) to also update
  for stopID, stop in pairs(global.LogisticTrainStops) do
    if not stop.entity.valid or not stop.input.valid or not stop.output.valid or not stop.lamp_control.valid then
      RemoveStop(stopID)
    else
      for i=#stop.active_deliveries, 1, -1 do --trainID should be unique => checking matching stop name not required
        if stop.active_deliveries[i] == old_train_id then
          if delivery then
            stop.active_deliveries[i] = new_train.id -- update train id if delivery exists
          else
            table.remove(stop.active_deliveries, i) -- otherwise remove entry
            if #stop.active_deliveries > 0 then
              setLamp(stop, "yellow", #stop.active_deliveries)
            else
              setLamp(stop, "green", 1)
            end
          end
        end
      end
    end
  end

  -- copy global.Dispatcher.Deliveries[old_train_id] to new_train.id and change attached train in delivery
  if delivery then
    delivery.train = new_train
    global.Dispatcher.Deliveries[new_train.id] = delivery
  end

  if global.StoppedTrains[old_train_id] then
    TrainLeaves(old_train_id) -- removal only, new train is added when on_train_state_changed fires with wait_station afterwards
  end
  global.Dispatcher.Deliveries[old_train_id] = nil

  return delivery
end

function OnTrainCreated(event)
  -- log("(on_train_created) Train name: "..tostring(Get_Train_Name(event.train))..", train.id:"..tostring(event.train.id)..", .old_train_id_1:"..tostring(event.old_train_id_1)..", .old_train_id_2:"..tostring(event.old_train_id_2)..", state: "..tostring(event.train.state))
  -- on_train_created always sets train.state to 9 manual, scripts have to set the train back to its former state.

  if event.old_train_id_1 then
    Update_Delivery(event.old_train_id_1, event.train)
  end

  if event.old_train_id_2 then
    Update_Delivery(event.old_train_id_2, event.train)
  end
end
