-- Show and modify properties of jobs in a workshop.
--[====[

gui/workshop-job
================
Run with a job selected in a workshop in the :kbd:`q` mode.

.. image:: /docs/images/workshop-job.png

The script shows a list of the input reagents of the selected job, and allows changing
them like the `job` ``item-type`` and `job` ``item-material`` commands.

Specifically, pressing the :kbd:`i` key pops up a dialog that lets you select an item
type from a list.

.. image:: /docs/images/workshop-job-item.png

Pressing :kbd:`m`, unless the item type does not allow a material,
lets you choose a material.

.. image:: /docs/images/workshop-job-material.png

Since there are a lot more materials than item types, this dialog is more complex
and uses a hierarchy of sub-menus. List choices that open a sub-menu are marked
with an arrow on the left.

.. warning::

  Due to the way input reagent matching works in DF, you must select an item type
  if you select a material, or the material will be matched incorrectly in some cases.
  If you press :kbd:`m` without choosing an item type, the script will auto-choose
  if there is only one valid choice, or pop up an error message box instead of the
  material selection dialog.

Note that both materials and item types presented in the dialogs are filtered
by the job input flags, and even the selected item type for material selection,
or material for item type selection. Many jobs would let you select only one
input item type.

For example, if you choose a *plant* input item type for your prepare meal job,
it will only let you select cookable materials.

If you choose a *barrel* item instead (meaning things stored in barrels, like
drink or milk), it will let you select any material, since in this case the
material is matched against the barrel itself. Then, if you select, say, iron,
and then try to change the input item type, now it won't let you select *plant*;
you have to unset the material first.

]====]

--@ module = true

local utils = require 'utils'
local gui = require 'gui'
local guidm = require 'gui.dwarfmode'
local guimat = require 'gui.materials'
local widgets = require 'gui.widgets'
local dlg = require 'gui.dialogs'

JobDetails = defclass(JobDetails, guidm.MenuOverlay)

JobDetails.focus_path = 'workshop-job'

JobDetails.ATTRS {
    job = DEFAULT_NIL,
    frame_inset = 1,
    frame_background = COLOR_BLACK,
}

function JobDetails:init(args)
    self.building = dfhack.job.getHolder(self.job)

    local status = { text = 'No worker', pen = COLOR_DARKGREY }
    local worker = dfhack.job.getWorker(self.job)
    if self.job.flags.suspend then
        status = { text = 'Suspended', pen = COLOR_RED }
    elseif worker then
        status = { text = dfhack.units.getReadableName(worker), pen = COLOR_GREEN }
    end

    self:addviews{
        widgets.Label{
            frame = { l = 0, t = 0 },
            text = {
                { text = df.job_type.attrs[self.job.job_type].caption }, NEWLINE, NEWLINE,
                '  ', status
            }
        },
        widgets.Label{
            frame = { l = 0, t = 4 },
            text = {
                { key = 'CUSTOM_I', text = ': Input item, ',
                  enabled = self:callback('canChangeIType'),
                  on_activate = self:callback('onChangeIType') },
                { key = 'CUSTOM_M', text = ': Material',
                  enabled = self:callback('canChangeMat'),
                  on_activate = self:callback('onChangeMat') }
            }
        },
        widgets.List{
            view_id = 'list',
            frame = { t = 6, b = 2 },
            row_height = 4,
            scroll_keys = widgets.SECONDSCROLL,
        },
        widgets.Label{
            frame = { l = 0, b = 0 },
            text = {
                { key = 'LEAVESCREEN', text = ': Back',
                  on_activate = self:callback('dismiss') }
            }
        },
    }

    self:initListChoices()
end

function JobDetails:onGetSelectedBuilding()
    return self.building
end

function JobDetails:onGetSelectedJob()
    return self.job
end

function describe_item_type(iobj)
    local itemline = 'any item'
    if iobj.item_type >= 0 then
        itemline = df.item_type.attrs[iobj.item_type].caption or iobj.item_type
        local def = dfhack.items.getSubtypeDef(iobj.item_type, iobj.item_subtype)
        local count = dfhack.items.getSubtypeCount(iobj.item_type, iobj.item_subtype)
        if def then
            itemline = def.name
        elseif count >= 0 then
            itemline = 'any '..itemline
        end
    end
    return itemline
end

function is_caste_mat(iobj)
    return dfhack.items.isCasteMaterial(iobj.item_type)
end

function describe_material(iobj)
    local matline = 'any material'
    if is_caste_mat(iobj) then
        matline = 'material not applicable'
    elseif iobj.mat_type >= 0 then
        local info = dfhack.matinfo.decode(iobj.mat_type, iobj.mat_index)
        if info then
            matline = info:toString()
        else
            matline = iobj.mat_type..':'..iobj.mat_index
        end
    end
    return matline
end

function list_flags(list, bitfield)
    for name,val in pairs(bitfield) do
        if val then
            table.insert(list, name)
        end
    end
end

function JobDetails:initListChoices()
    local items = {}
    for i,ref in ipairs(self.job.items) do
        local idx = ref.job_item_idx
        if idx >= 0 then
            items[idx] = (items[idx] or 0) + 1
        end
    end

    local choices = {}
    for i,iobj in ipairs(self.job.job_items.elements) do
        local head = 'Item '..(i+1)..': '..(items[i] or 0)..' of '..iobj.quantity
        if iobj.min_dimension > 0 then
            head = head .. '(size '..iobj.min_dimension..')'
        end

        local line1 = {}
        local reaction = df.reaction.find(iobj.reaction_id)
        if reaction and #iobj.contains > 0 then
            for _,ri in ipairs(iobj.contains) do
                table.insert(line1, 'has '..utils.call_with_string(
                    reaction.reagents[ri],'getDescription',iobj.reaction_id
                ))
            end
        end
        if iobj.metal_ore >= 0 then
            local ore = dfhack.matinfo.decode(0, iobj.metal_ore)
            if ore then
                table.insert(line1, 'ore of '..ore:toString())
            end
        end
        if iobj.has_material_reaction_product ~= '' then
            table.insert(line1, 'product '..iobj.has_material_reaction_product)
        end
        if iobj.reaction_class ~= '' then
            table.insert(line1, 'class '..iobj.reaction_class)
        end
        if iobj.has_tool_use >= 0 then
            table.insert(line1, 'has use '..df.tool_uses[iobj.has_tool_use])
        end
        list_flags(line1, iobj.flags1)
        list_flags(line1, iobj.flags2)
        list_flags(line1, iobj.flags3)
        if #line1 == 0 then
            table.insert(line1, 'no flags')
        end

        table.insert(choices, {
            index = i,
            iobj = iobj,
            text = {
                head, NEWLINE,
                '  ', { text = curry(describe_item_type, iobj) }, NEWLINE,
                '  ', { text = curry(describe_material, iobj) }, NEWLINE,
                '  ', table.concat(line1, ', '), NEWLINE
            }
        })
    end

    self.subviews.list:setChoices(choices)
end

function JobDetails:canChangeIType()
    local idx, obj = self.subviews.list:getSelected()
    return obj ~= nil
end

function JobDetails:setItemType(obj, item_type, item_subtype)
    obj.iobj.item_type = item_type
    obj.iobj.item_subtype = item_subtype

    if is_caste_mat(obj.iobj) then
        self:setMaterial(obj, -1, -1)
    end
end

function JobDetails:onChangeIType()
    local idx, obj = self.subviews.list:getSelected()
    guimat.ItemTypeDialog{
        prompt = 'Please select a new item type for input '..idx,
        none_caption = 'any item',
        item_filter = curry(dfhack.job.isSuitableItem, obj.iobj),
        on_select = self:callback('setItemType', obj)
    }:show()
end

function JobDetails:canChangeMat()
    local idx, obj = self.subviews.list:getSelected()
    return obj ~= nil and not is_caste_mat(obj.iobj)
end

function JobDetails:setMaterial(obj, mat_type, mat_index)
    if  obj.index == 0
    and self.job.mat_type == obj.iobj.mat_type
    and self.job.mat_index == obj.iobj.mat_index
    and self.job.job_type ~= df.job_type.PrepareMeal
    then
        self.job.mat_type = mat_type
        self.job.mat_index = mat_index
    end

    obj.iobj.mat_type = mat_type
    obj.iobj.mat_index = mat_index
end

function JobDetails:findUnambiguousItem(iobj)
    local count = 0
    local itype

    for i = 0,df.item_type._last_item do
        if dfhack.job.isSuitableItem(iobj, i, -1) then
            count = count + 1
            if count > 1 then return nil end
            itype = i
        end
    end

    return itype
end

function JobDetails:onChangeMat()
    local idx, obj = self.subviews.list:getSelected()

    if obj.iobj.item_type == -1 and obj.iobj.mat_type == -1 then
        -- If the job allows only one specific item type, use it
        local vitype = self:findUnambiguousItem(obj.iobj)

        if vitype then
            obj.iobj.item_type = vitype
        else
            dlg.showMessage(
                'Bug Alert',
                { 'Please set a specific item type first.\n\n',
                  'Otherwise the material will be matched\n',
                  'incorrectly due to a limitation in DF code.' },
                COLOR_YELLOW
            )
            return
        end
    end

    guimat.MaterialDialog{
        prompt = 'Please select a new material for input '..idx,
        none_caption = 'any material',
        mat_filter = function(mat,parent,mat_type,mat_index)
            return dfhack.job.isSuitableMaterial(obj.iobj, mat_type, mat_index, obj.iobj.item_type)
        end,
        on_select = self:callback('setMaterial', obj)
    }:show()
end

function JobDetails:onInput(keys)
    if self:propagateMoveKeys(keys) then
        if df.global.world.selected_building ~= self.building then
            self:dismiss()
        end
    else
        JobDetails.super.onInput(self, keys)
    end
end

if dfhack_flags.module then
    return
end

if not string.match(dfhack.gui.getCurFocus(), '^dwarfmode/QueryBuilding/Some/Workshop/Job') then
    qerror("This script requires a workshop job selected in the 'q' mode")
end

local dlg = JobDetails{ job = dfhack.gui.getSelectedJob() }
dlg:show()
