--[[
	This file is part of NeuralView.

    NeuralView is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    NeuralView is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with NeuralView.  If not, see <http://www.gnu.org/licenses/>.
--]]

---
-- Dialog that handles a list of filters.
--
-- To store the filters and the relations, the following data structures
-- are used:
-- <ul>
-- <li>self.filters: array that contains the current filters
-- (may have 'holes') </li>
-- <li>self.model: list in the form (filtername, self.filters index) </li>
-- <li>self.neurons_tab.model: list in the form (neuron identifier, 
-- self.filters index)</li>
-- </ul>
local FilterManager, FilterManagerMT = nv_module("view.FilterManager")

local Filter = require("core.Filter")

local NO_FILTER = {name = _"No filter"} -- dummy

---
-- Constructor.
--
-- @param parent Parent window
-- @return New instance of FilterManager 
function FilterManager.new(parent)
	local self = {}
	setmetatable(self, FilterManagerMT)
	
	self:build_gui(parent)
	self.iter = gtk.TreeIter.new()
	self.neurons = {}
	
	return self
end

---
-- Builds the filter creation / edition / removal tab.
--
-- @private
function FilterManager:build_filter_tab()
	local tab = {}
	self.filter_tab = tab
	
	tab.hbox = gtk.HBox.new(false, 5)
	tab.button_box = gtk.VButtonBox.new()
	tab.button_box:set("layout-style", gtk.BUTTONBOX_START)
	
	tab.add = gtk.Button.new_from_stock("gtk-add")
	tab.add:connect("clicked", self.add_filter, self)
	tab.add:set("tooltip-text", _"Add a new filter")
	
	tab.edit = gtk.Button.new_from_stock("gtk-edit")
	tab.edit:connect("clicked", self.edit_filter, self)
	tab.edit:set("tooltip-text", _"Edit the selected filter")
	
	tab.remove = gtk.Button.new_from_stock("gtk-remove")
	tab.remove:connect("clicked", self.remove_filter, self)
	tab.remove:set("tooltip-text", _"Remove the selected filter")
	
	tab.button_box:add(tab.add, tab.edit, tab.remove)
	
	self.model = gtk.ListStore.new("gchararray", "gint")
	tab.view = gtk.TreeView.new_with_model(self.model)
	tab.view:set("enable-search", false, "enable-tree-lines", true,
		"reorderable", true)
	tab.selection = tab.view:get_selection()
	tab.selection:connect("changed", self.selection_changed, self)
	
	tab.cell_renderer = gtk.CellRendererText.new()
	tab.cell_renderer:set("editable", true)
	tab.cell_renderer:connect("edited", self.cell_edited, self)
	tab.column = gtk.TreeViewColumn.new_with_attributes(_"Filter name",
		tab.cell_renderer, "text", 0)
	tab.view:append_column(tab.column)
	
	tab.scroll = gtk.ScrolledWindow.new()
	tab.scroll:set("hscrollbar-policy", gtk.POLICY_AUTOMATIC,
		"vscrollbar-policy", gtk.POLICY_AUTOMATIC)
	tab.scroll:add(tab.view)
	
	tab.hbox:pack_start(tab.scroll, true, true, 0)
	tab.hbox:pack_start(tab.button_box, false, true, 0)
	
	tab.label = gtk.Label.new_with_mnemonic(_"_Filters")
	tab.label:set("tooltip-text", _"Available filters")
end

---
-- Builds the neurons tab.
--
-- @private
function FilterManager:build_neurons_tab()
	local tab = {}
	self.neurons_tab = tab
	
	tab.model = gtk.ListStore.new("gchararray", "gint")
	tab.view = gtk.TreeView.new_with_model(tab.model)
	tab.neuron_cell = gtk.CellRendererText.new()
	tab.neuron_col = gtk.TreeViewColumn.new_with_attributes(_"Neuron",
		tab.neuron_cell, "text", 0)
	tab.filter_cell = gtk.CellRendererCombo.new()
	tab.filter_cell:set("model", self.model, "text-column", 0,
		"has-entry", false, "editable", true)
	tab.filter_cell:connect("changed", self.neurons_filter_changed, self)
	tab.filter_cell:connect("edited", self.neurons_filter_edited, self)
	tab.filter_col = gtk.TreeViewColumn.new_with_attributes(_"Filter",
		tab.filter_cell, "text", 1)
	tab.filter_col:set_cell_data_func(tab.filter_cell,
		self.neurons_filter_cell_data_func, self)
	
	tab.view:append_column(tab.neuron_col)
	tab.view:append_column(tab.filter_col)
	
	tab.scroll = gtk.ScrolledWindow.new()
	tab.scroll:set("hscrollbar-policy", gtk.POLICY_AUTOMATIC, 
		"vscrollbar-policy", gtk.POLICY_AUTOMATIC)
	tab.scroll:add(tab.view)
	
	tab.label = gtk.Label.new_with_mnemonic(_"_Neurons")
	tab.label:set("tooltip-text", _"Filter associations")
end

---
-- Builds the interface.
--
-- @param parent Parent window
-- @private
function FilterManager:build_gui(parent)
	self.dialog = gtk.Dialog.new()
	self.dialog:set("transient-for", parent, "title", _"Filter manager",
		"default-width", 400, "default-height", 300, "icon", NVIcon)
	self.dialog:add_button("gtk-close", gtk.RESPONSE_OK)
	
	self.vbox = self.dialog:get_content_area()
	self.notebook = gtk.Notebook.new()
	
	self:build_filter_tab()
	self.notebook:append_page(self.filter_tab.hbox, self.filter_tab.label)
	
	self:build_neurons_tab()
	self.notebook:append_page(self.neurons_tab.scroll, self.neurons_tab.label)
	
	self.vbox:add(self.notebook)
	self.vbox:show_all()
end

---
-- @private
function FilterManager:neurons_filter_changed(path, iter)
	-- set the neuron filter
	local neuron = tonumber(path) + 1
	local filter = self.model:get(iter, 1)
	self.neurons[neuron] = filter
end

---
-- @private
function FilterManager:neurons_filter_edited(path)
	local neuron = tonumber(path) + 1
	self.neurons_tab.model:get_iter_from_string(self.iter, path)
	self.neurons_tab.model:set(self.iter, 1, self.neurons[neuron])
end

---
-- @private
function FilterManager:neurons_filter_cell_data_func(iter)
	local filter = self.neurons_tab.model:get(iter, 1)
	filter = self.filters[filter]
	
	-- if the filter doesn't exists them it's because it was removed
	-- all these special cases must be handled in self:save_filters()
	if filter then
		name = filter.name
	else
		name = NO_FILTER.name
	end
	
	self.neurons_tab.filter_cell:set("text", name)
end

---
-- @private
function FilterManager:add_helper(filter)
	self.model:append(self.iter)
	self.model:seto(self.iter, filter.name, self.filter_index)
	self.filters[self.filter_index] = filter
	self.filter_index = self.filter_index + 1
end

---
-- @private
function FilterManager:add_filter()
	self:add_helper(Filter.new())
end

---
-- @private
function FilterManager:edit_filter()
	if self:has_selection() then
		if not self.editor then
			local FilterEditor = require("view.FilterEditor")
			self.editor = FilterEditor.new(self.dialog)
		end
	
		local id = self.model:get(self.iter, 1)
		local new_filter = self.editor:run(self.filters[id])
		
		new_filter.name = self.filters[id].name
		new_filter.cols = self.filters[id].cols
		
		self.filters[id] = new_filter
		self.data_changed = true
	end
end

---
-- @private
function FilterManager:remove_filter()
	if self:has_selection() then
		local id = self.model:get(self.iter, 1)
		self.model:remove(self.iter)
		self.filters[id] = nil -- release the reference to allow gc
		self.data_changed = true
	end
end

---
-- @private
function FilterManager:has_selection()
	local has_sel = self.filter_tab.selection:get_selected(self.iter)
	
	if has_sel then
		local filter = self.filters[self.model:get(self.iter, 1)]
		has_sel = filter ~= NO_FILTER
	end
	
	return has_sel
end

---
-- @private
function FilterManager:selection_changed()
	local tab = self.filter_tab
	local has_sel = self:has_selection()
	tab.edit:set("sensitive", has_sel)
	tab.remove:set("sensitive", has_sel)
end

---
-- @private
function FilterManager:cell_edited(path, data)
	if self:has_selection() then
		if not data or data == "" then return false end
		self.model:get_iter_from_string(self.iter, path)
		
		local filter = self.model:get(self.iter, 1)
		self.filters[filter].name = data
		
		self.model:set(self.iter, 0, data)
		self.data_changed = true
	end
end

---
-- Prepares the manager for an array of filters.
--
-- @param project Project that contains the filters
-- @private
function FilterManager:prepare(project)
	self.project = project
	self.filter_index = 0
	self.filters = {}
	
	self.filter_tab.view:set("model", nil)
	self.neurons_tab.view:set("model", nil)
	self.model:clear()
	self.neurons_tab.model:clear()
	
	if project then
		-- the mighty "No filter"
		self:add_helper(NO_FILTER)
		
		-- read the relations from the filters
		local rel = {}
	
		-- populate the filters	
		for i, filter in ipairs(project.filters) do
			self:add_helper(filter)
			
			for j, r in ipairs(filter.cols) do
				rel[r] = i
			end
		end
		
		local model, network = self.neurons_tab.model, project.network
		
		-- add the input neurons
		local num_input = network:get_num_input()
		
		for i = 1, num_input do
			model:append(self.iter)
			model:seto(self.iter, _"Input#" .. i, rel[i] or 0)
		end
		
		-- add the output neurons
		local num_output = network:get_num_output()
		
		for i = 1, num_output do
			model:append(self.iter)
			model:seto(self.iter, _"Output#" .. i, rel[i + num_input] or 0)
		end
	end
	
	self.filter_tab.view:set("model", self.model)
	self.neurons_tab.view:set("model", self.neurons_tab.model)
end

---
-- Saves the filters back to the project.
--
-- @private
function FilterManager:save_filters()
	-- holes can be present in the filter table, so we must skip them and
	-- shift the filters 'to zero'
	
	-- get the references, with the original ids
	local refs = {} -- map<filter_id, {neuron_id} >
	local tab = self.neurons_tab
	
	local valid, i = tab.model:get_iter_first(self.iter), 1
	
	while valid do
		local filter_id = tab.model:get(self.iter, 1)
		if not refs[filter_id] then refs[filter_id] = {} end 
		table.insert(refs[filter_id], i)
		
		valid = tab.model:iter_next(self.iter)
		i = i + 1
	end
	
	-- get the filters and their relations from the refs table, and store then
	-- in a new array, without holes
		
	local new_filters = {}
	local valid = self.model:get_iter_first(self.iter)
	
	while valid do
		local filter_id = self.model:get(self.iter, 1)
		local filter = self.filters[filter_id]
		
		if filter and filter ~= NO_FILTER then
			filter.cols = refs[filter_id] or {}
			table.insert(new_filters, filter)
		end
		
		valid = self.model:iter_next(self.iter)
	end
	
	self.project.filters = new_filters
end

---
-- Runs the dialog.
--
-- @param project Project that contains the filters
-- @return true if the filters changed
function FilterManager:run(project)
	self.data_changed = false
	self:prepare(project)
	
	self.dialog:run()
	self.dialog:hide()
	
	self:save_filters()
	self:prepare()
	
	return self.data_changed
end

return FilterManager
