local tnt = require 'torchnet.env'

local tester
local test = torch.TestSuite()

function test.AverageValueMeter()
   local mtr = tnt.AverageValueMeter()

   mtr:add(1)
   local avg, var = mtr:value()

   tester:eq(avg, 1)
   tester:assert(var ~= var, "Variance for a single value is undefined")

   mtr:add(3)
   avg, var = mtr:value()

   tester:eq(avg, 2)
   tester:eq(var, math.sqrt(2))
end

function test.ClassErrorMeter()
   local mtr = tnt.ClassErrorMeter{topk = {1}}

   local output = torch.Tensor({{1,0,0},{0,1,0},{0,0,1}})
   local target = torch.Tensor({1,2,3})
   mtr:add(output, target)
   local error = mtr:value()

   tester:eq(error, {0}, "All should be correct")

   target[1] = 2
   target[2] = 1
   target[3] = 1
   mtr:add(output, target)

   error = mtr:value()
   tester:eq(error, {50}, "Half, i.e. 50%, should be correct")
end

function test.ConfusionMeter()
   local mtr = tnt.ConfusionMeter{k = 3}

   -- The max value is the one that is correct
   local output = torch.Tensor({{.8,0.1,0.1},{10,11,10},{0.2,0.2,.3}})
   local target = torch.Tensor({1,2,3})
   mtr:add(output, target)
   local conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), 3, "All should be correct")
   tester:eq(torch.diag(conf_mtrx):sum(), 3, "All should be correct")

   target[1] = 2
   target[2] = 1
   target[3] = 1
   mtr:add(output, target)

   tester:eq(conf_mtrx:sum(), 6, "Six tests should give six values")
   tester:eq(torch.diag(conf_mtrx):sum(), 3, "Shouldn't have changed since all new values were false")
   tester:eq(conf_mtrx[1]:sum(), 3, "All top have gotten one guess")
   tester:eq(conf_mtrx[2]:sum(), 2, "Two first at the 2nd row have a guess")
   tester:eq(conf_mtrx[2][3], 0, "The last one should be empty")
   tester:eq(conf_mtrx[3]:sum(), 1, "Bottom row has only the first test correct")
   tester:eq(conf_mtrx[3][3], 1, "Bottom row has only the first test correct")

   -- Test normalized version
   mtr = tnt.ConfusionMeter{k = 4, normalized=true}
   output = torch.Tensor({
      {.8,0.1,0.1,0},
      {10,11,10,0},
      {0.2,0.2,.3,0},
      {0,0,0,1}
   })

   target = torch.Tensor({1,2,3,4})
   mtr:add(output, target)
   conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), output:size(2), "All should be correct")
   tester:eq(torch.diag(conf_mtrx):sum(), output:size(2), "All should be correct")

   target[1] = 2
   target[2] = 1
   target[3] = 1
   mtr:add(output, target)
   conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), output:size(2), "The noramlization should sum all values to 1")
   for i=1,output:size(2) do
      tester:eq(conf_mtrx[i]:sum(), 1, "Row no " .. i .. " fails to sum to one in normalized mode")
   end
end

function test.MultilabelConfusionMeter()
   local mtr = tnt.MultiLabelConfusionMeter{k = 3, normalized=false}

   -- The max value is the one that is correct
   local output = torch.Tensor({{.8,0.1,0.1},{10,11,10},{0.2,0.2,.3}})
   local target = torch.LongTensor({1,2,3})
   local one_hot = torch.zeros(output:size())
   one_hot:scatter(2, target:view(-1,1), 1)
   mtr:add(output, one_hot)
   local conf_mtrx = mtr:value()

   tester:eq(conf_mtrx, torch.eye(3), "All should be correct")

   target[1] = 2
   target[2] = 1
   target[3] = 1
   one_hot = torch.zeros(output:size())
   one_hot:scatter(2, target:view(-1,1), 1)
   mtr:add(output, one_hot)
   conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), 6, "Six tests should give six values")
   tester:eq(torch.diag(conf_mtrx):sum(), 3, "Shouldn't have changed since all new values were false")
   tester:eq(conf_mtrx[1]:sum(), 3, "All top have gotten one guess")
   tester:eq(conf_mtrx[2]:sum(), 2, "Two first at the 2nd row have a guess")
   tester:eq(conf_mtrx[2][3], 0, "The last one should be empty")
   tester:eq(conf_mtrx[3]:sum(), 1, "Bottom row has only the first test correct")
   tester:eq(conf_mtrx[3][3], 1, "Bottom row has only the first test correct")

   -- Test normalized version
   mtr = tnt.MultiLabelConfusionMeter{k = 4, normalized=true}
   output = torch.Tensor({
      {.8,0.1,0.1,0},
      {10,11,10,0},
      {0.2,0.2,.3,0},
      {0,0,0,1}
   })

   target = torch.LongTensor({1,2,3,4})
   one_hot = torch.zeros(output:size())
   one_hot:scatter(2, target:view(-1,1), 1)
   mtr:add(output, one_hot)
   conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), output:size(2), "All should be correct", 10^-3)
   tester:eq(torch.diag(conf_mtrx):sum(), output:size(2), "All should be correct", 10^-3)

   target[1] = 2
   target[2] = 1
   target[3] = 1
   one_hot = torch.zeros(output:size())
   one_hot:scatter(2, target:view(-1,1), 1)
   mtr:add(output, one_hot)
   conf_mtrx = mtr:value()

   tester:eq(conf_mtrx:sum(), output:size(2), "The noramlization should sum all values to 1", 10^-3)
   for i=1,output:size(2) do
      tester:eq(conf_mtrx[i]:sum(), 1, "Row no " .. i .. " fails to sum to one in normalized mode", 10^-3)
   end
end

function test.AUCMeter()
   local mtr = tnt.AUCMeter()

   torch.manualSeed(41412)
   local test_size = 10^3
   mtr:add(torch.rand(test_size), torch.zeros(test_size))
   mtr:add(torch.rand(test_size), torch.Tensor(test_size):fill(1))
   local err = mtr:value()
   tester:eq(err, 0.5, "Random guesses should provide a AUC close to 0.5", 10^-1)

   mtr:reset()
   mtr:add(torch.Tensor(test_size):fill(0), torch.zeros(test_size))
   mtr:add(torch.Tensor(test_size):fill(.1), torch.zeros(test_size))
   mtr:add(torch.Tensor(test_size):fill(.2), torch.zeros(test_size))
   mtr:add(torch.Tensor(test_size):fill(.3), torch.zeros(test_size))
   mtr:add(torch.Tensor(test_size):fill(.4), torch.zeros(test_size))
   mtr:add(torch.Tensor(test_size):fill(1), torch.Tensor(test_size):fill(1))
   err = mtr:value()
   tester:eq(err, 1, "Only correct guesses should provide a AUC close to 1", 10^-1)
end


function test.APMeter()
   local mtr = tnt.APMeter()

   local target = torch.Tensor{0,   1, 0,   1}
   local output = torch.Tensor{.1,  0.2, 0.3, 4}
   local weight = torch.Tensor{0.5, 1.0, 2.0, 0.1}
   mtr:add(output, target, weight)

   local ap = mtr:value()
   tester:eq(
      ap[1], (1*0.1/0.1 + 0*2.0/2.1 + 1.1*1/3.1+ 0*1/4)/2.0,
      'aptest1 failed'
   )

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(ap[1], (1*1/1 + 0*1/2 + 2*1/3 + 0*1/4)/2, 'aptest2 failed')

   target = torch.Tensor{0,1,0,1}
   output = torch.Tensor{4,3,2,1}
   weight = torch.Tensor{1,2,3,4}

   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(ap[1], (0*1/1 + 1*2/3 + 2*0/6 + 6*1/10)/2, 'aptest3 failed')

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(ap[1], (0*1 + 1*1/2 + 0*1/3 + 2*1/4)/2, 'aptest4 failed')

   target = torch.Tensor{0,1,0,1}
   output = torch.Tensor{1,4,2,3}
   weight = torch.Tensor{1,2,3,4}
   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(ap[1], (4*1/4 + 6*1/6 + 0*6/9 + 0*6/10)/2, 'aptest5 failed')

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(ap[1], (1*1 + 2*1/2 + 0*1/3 + 0*1/4)/2, 'aptest6 failed')

   target = torch.Tensor{0,0,0,0}
   output = torch.Tensor{1,4,2,3}
   weight = torch.Tensor{1.0, 0.1, 0.0, 0.5}
   mtr:reset()
   mtr:add(output, target, weight)

   ap = mtr:value()
   tester:eq(ap[1], 0)

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(ap[1], 0)

   target = torch.Tensor{1,1,0}
   output = torch.Tensor{3,1,2}
   weight = torch.Tensor{1,0.1,3}
   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(ap[1], (1*1/1 + 1*0/4 + 1.1/4.1)/2, 'aptest7 failed')

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(ap[1], (1*1 + 0*1/2 + 2*1/3)/2, 'aptest8 failed')

   -- Test multiple K:s
   target = torch.Tensor{
      {0,1,0,1},
      {0,1,0,1}
   }:transpose(1,2)
   output = torch.Tensor{
      {.1,.2,.3,4},
      {4,3,2,1}
   }:transpose(1,2)
   weight = torch.Tensor{
     {1.0, 0.5, 2.0, 3.0},
   }:transpose(1,2)

   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(
      ap,
      torch.DoubleTensor{
         (1*3.0/3.0 + 0*3.0/5.0 + 3.5*1/5.5 + 0*3.5/6.5)/2,
         (0*1.0/1.0 + 1*0.5/1.5 + 0*0.5/3.5 + 1*3.5/6.5)/2
      },
      'aptest9 failed'
   )

   mtr:reset()
   mtr:add(output, target)
   ap = mtr:value()
   tester:eq(
      ap,
      torch.DoubleTensor{
         (1*1 + 0*1/2 + 2*1/3 + 0*1/4)/2,
         (0*1 + 1*1/2 + 0*1/3 + 2*1/4)/2
      },
      'aptest10 failed'
   )

   mtr:reset()
   output = torch.DoubleTensor(5, 4):fill(.25)
   target = torch.LongTensor(5, 4):fill(1)
   mtr:add(output, target)
   output = torch.DoubleTensor(1, 4):fill(.25)
   target = torch.LongTensor(1, 4):fill(1)
   mtr:add(output, target)
   tester:assert(mtr:value(), 'aptest11 failed')
end


function test.mAPMeter()
   local mtr = tnt.mAPMeter()

   local target = torch.Tensor{0,1,0,1}
   local output = torch.Tensor{.1,.2,.3,4}
   local weight = torch.Tensor{0.5, 1.0, 2.0, 0.1}
   mtr:add(output, target)

   local ap = mtr:value()
   tester:eq(ap, (1*1 + 0*1/2 + 2*1/3 + 0*1/4)/2)

   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(ap, (1*0.1/0.1 + 0*2.0/2.1 + 1.1*1/3.1+ 0*1/4)/2.0)

   -- Test multiple K:s
   mtr:reset()
   target = torch.Tensor{
      {0,1,0,1},
      {0,1,0,1}
   }:transpose(1,2)
   output = torch.Tensor{
      {.1,.2,.3,4},
      {4,3,2,1}
   }:transpose(1,2)
   weight = torch.Tensor{
     {1.0, 0.5, 2.0, 3.0},
   }:transpose(1,2)

   mtr:add(output, target)

   ap = mtr:value()
   tester:eq(
      ap,
      torch.DoubleTensor{
         (1*1 + 0*1/2 + 2*1/3 + 0*1/4)/2,
         (0*1 + 1*1/2 + 0*1/3 + 2*1/4)/2
      }:mean()
   )

   mtr:reset()
   mtr:add(output, target, weight)
   ap = mtr:value()
   tester:eq(
      ap,
      torch.DoubleTensor{
         (1*3.0/3.0 + 0*3.0/5.0 + 3.5*1/5.5 + 0*3.5/6.5)/2,
         (0*1.0/1.0 + 1*0.5/1.5 + 0*0.5/3.5 + 1*3.5/6.5)/2
      }:mean()
   )
end


function test.MovingAverageValueMeter()
   -- Moving average meter with windowsize = 3
   local mtr = tnt.MovingAverageValueMeter(3)

   mtr:add(1)
   local avg, var = mtr:value()

   tester:eq(avg, 1)
   tester:eq(var, 0)

   mtr:add(3)
   avg, var = mtr:value()

   tester:eq(avg, 2)
   tester:eq(var, math.sqrt(2))

   mtr:add(5)
   avg, var = mtr:value()

   tester:eq(avg, 3)
   tester:eq(var, 2)

   mtr:add(4)
   avg, var = mtr:value()

   tester:eq(avg, 4)
   tester:eq(var, 1)

   mtr:add(0)
   avg, var = mtr:value()

   tester:eq(avg, 3)
   tester:eq(var, math.sqrt(7))
end


function test.NDCGMeter()
   local mtr = tnt.NDCGMeter{K = {6}}

   -- From: https://en.wikipedia.org/wiki/Discounted_cumulative_gain#Normalized_DCG
   local relevance = torch.DoubleTensor{3,2,3,0,1,2}
   local output = torch.linspace(relevance:size(1), 1, relevance:size(1)):double()
   mtr:add(output, relevance)

   local est = mtr:value()
   tester:eq(est[6], 0.932, "Problematic nDGC with K=6", 10^-3)
end

function test.PrecisionMeter()
   local mtr = tnt.PrecisionMeter{}

   local target = torch.zeros(10)
   target:narrow(1, 3, 7):fill(1)
   local output = torch.zeros(10)
   output:narrow(1, 2, 6):fill(1)

   mtr:add(output, target)

   local tp = 5
   local fp = 1
   tester:eq(mtr:value()[0.5], 100 * tp / (tp + fp), "Basic test", 10^-2)

   mtr = tnt.PrecisionMeter{
      threshold = {.5, .7}
   }

   target = torch.zeros(10)
   target:narrow(1, 3, 7):fill(1)
   output = torch.zeros(10)
   output:narrow(1, 2, 3):fill(.5)
   output:narrow(1, 5, 3):fill(.7)

   mtr:add(output, target)
   tp = 5
   fp = 1
   tester:eq(mtr:value()[0.5], 100 * tp / (tp + fp), "Cutoff at 0.5", 10^-2)
   tester:eq(mtr:value()[0.7], 100, "Cutoff at 0.7", 10^-2)
end

function test.PrecisionAtKMeter()
   local mtr = tnt.PrecisionAtKMeter{topk = {1, 2, 3}}

   local target = torch.eye(3)
   local output = torch.Tensor{
      {.5,.1,.4},
      {.1,.5,.4},
      {.1,.4,.5}
   }
   mtr:add(output, target)
   tester:eq(mtr:value()[1], 100*3/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*3/(3*2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*3/(3*3), "Top 3 matches", 10^-3)

   mtr:add(output, target) -- Adding the same twice shouldn't change anything
   tester:eq(mtr:value()[1], 100*3/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*3/(3*2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*3/(3*3), "Top 3 matches", 10^-3)

   mtr:reset()
   target[1][3] = 1

   mtr:add(output, target)
   tester:eq(mtr:value()[1], 100*3/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*(3 + 1)/(3 * 2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*(3 + 1)/(3 * 3), "Top 3 matches", 10^-3)

   mtr:reset()
   output = torch.Tensor{
      {.1,.5,.4},
      {.1,.5,.4},
      {.5,.4,.1}
   }
   mtr:add(output, target)
   tester:eq(mtr:value()[1], 100*1/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*(1 + 1)/(3 * 2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*(1 + 1 + 2)/(3 * 3), "Top 3 matches", 10^-3)

   local mtr = tnt.PrecisionAtKMeter{topk = {1, 2, 3}, online = true}

   local target = torch.eye(3)
   local output = torch.Tensor{
      {.5,.1,.4},
      {.1,.5,.4},
      {.1,.4,.5}
   }
   mtr:add(output, target)
   tester:eq(mtr:value()[1], 100*3/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*3/(3*2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*3/(3*3), "Top 3 matches", 10^-3)

   mtr:add(output, target)
   tester:eq(mtr:value()[1], 100*3/3, "Top 1 matches", 10^-3)
   tester:eq(mtr:value()[2], 100*3*2/(3*2), "Top 2 matches", 10^-3)
   tester:eq(mtr:value()[3], 100*3*2/(3*3), "Top 3 matches", 10^-3)
end

function test.RecallMeter()
   local mtr = tnt.RecallMeter{}

   local target = torch.zeros(10)
   target:narrow(1, 3, 7):fill(1)
   local output = torch.zeros(10)
   output:narrow(1, 2, 6):fill(1)

   mtr:add(output, target)

   local tp = 5
   local fn = 2
   tester:eq(mtr:value()[0.5], 100 * tp / (tp + fn), "Basic test", 10^-2)

   mtr = tnt.RecallMeter{
      threshold = {.5, .7}
   }

   target = torch.zeros(10)
   target:narrow(1, 3, 7):fill(1)
   output = torch.zeros(10)
   output:narrow(1, 2, 3):fill(.5)
   output:narrow(1, 5, 3):fill(.7)

   mtr:add(output, target)
   tester:eq(mtr:value()[0.5], 100 * tp / (tp + fn), "Cutoff at 0.5", 10^-2)
   tp = tp - 2
   fn = fn + 2
   tester:eq(mtr:value()[0.7], 100 * tp / (tp + fn), "Cutoff at 0.7", 10^-2)
end

function test.TimeMeter()
   local mtr = tnt.TimeMeter()

   local function wait(seconds)
     local start = os.time()
     repeat until os.time() > start + seconds
   end

   mtr:reset()
   wait(1)
   local passed_time = mtr:value()
   tester:assert(passed_time < 2,
                ("Too long time passed: %.1f sec >= 2 sec"):format(passed_time))
   tester:assert(passed_time > .5,
                ("Too short time passed:  %.1f sec <= 0.5 sec"):format(passed_time))
end

return function(_tester_)
   tester = _tester_
   return test
end
