# Copyright (c) 2012 by Zuse-Institute Berlin and the Technical University of Denmark.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#     1. Redistributions of source code must retain the above copyright
#        notice, this list of conditions and the following disclaimer.
#     2. Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#     3. Neither the name of the copyright holders nor contributors may not
#        be used to endorse or promote products derived from this software
#        without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS NOR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


# Direct execution requires top level directory on python path
if __name__ == "__main__":
  import os, sys, inspect
  scriptdir = os.path.split(inspect.getfile( inspect.currentframe() ))[0]
  packagedir = os.path.realpath(os.path.abspath(os.path.join(scriptdir,'..')))
  if packagedir not in sys.path:
    sys.path.insert(0, packagedir)


import os, inspect, csv
from filter import filter

statdictnames = ['pcer','perr','pobj','dcer','derr','dobj','claim','comment']


class latextable:
  def __init__(self, statdict):
    self.statdict = statdict
    self.objstatmark = ['\\hphantom{\\tnote{a}}', '\\tnote{v}', '\\tnote{a}']

  def opentable(self):
    print('''\
% Requires package "longtable", "booktabs" and "threeparttable"
\\setlength{\\LTleft}{-20cm plus -1fill}
\\setlength{\\LTright}{\\LTleft}
\\begin{scriptsize}
\\begin{ThreePartTable}
\\begin{TableNotes}
\\item[a(currancy)] Infeasibility meassures exceed $10^{-4}$ on some primitive cones or integer requirements (points not normalized)
\item[v(alue)] Objective neither claimed by a solver (mixed-integer case) nor certified by a feasible dual solution (continuous case), to be within an absolute gap of $10^{-4}$ or relative gap of $10^{-7}$ from optimality.
\\end{TableNotes}
\\begin{longtable}{l|rrr|rrrrrr|rr|r}
\\toprule
\\multicolumn{1}{l}{} & \\multicolumn{3}{c}{} & \\multicolumn{6}{c}{\\bf Conic domain entries} & \\multicolumn{2}{c}{} & \\multicolumn{1}{c}{} \\\\
\\multicolumn{1}{l}{} & \\multicolumn{3}{c}{\\bf Size} & \\multicolumn{3}{c}{\\bf Linear} & \\multicolumn{3}{c}{\\bf Second-order} & \\multicolumn{2}{c}{\\bf Conic domains} & \\multicolumn{1}{c}{\\bf Objective} \\\\
\\cmidrule(r){2-4} \\cmidrule(l){5-7} \\cmidrule(r){8-10} \\cmidrule(lr){11-12} \\cmidrule(l){13-13}
\\multicolumn{1}{l}{\\bf Instances} & \\multicolumn{1}{r}{var} & \\multicolumn{1}{r}{map} & \\multicolumn{1}{r}{nnz} & \\multicolumn{1}{r}{bin} & \\multicolumn{1}{r}{int} & \\multicolumn{1}{r}{cont} & \\multicolumn{1}{r}{bin} & \\multicolumn{1}{r}{int} & \\multicolumn{1}{r}{cont} & \\multicolumn{1}{r}{lin} & \\multicolumn{1}{r}{so} & \\multicolumn{1}{r}{} \\\\
\\midrule''')

  def closetable(self):
    print('''\
\\bottomrule
\\hiderowcolors
\insertTableNotes\\\\
\\hiderowcolors
\\caption{\\cblibstattablecaption}
\\label{cblib:stattable}
\\end{longtable}
\\end{ThreePartTable}
\\end{scriptsize}''')

  def addrow(self, data):
    column = ['']*13

    # Name
    column[0] = data[0].replace('_','\_')

    # var, map, nnz
    column[1] = str(data[1])
    column[2] = str(data[2])
    column[3] = str(data[3])

    # bin, int, cont (linear)
    if data[4] != 0:
      column[4] = str(data[4])
    if data[5]-data[4] != 0:
      column[5] = str(data[5]-data[4])
    if data[6]-data[5] != 0:
      column[6] = str(data[6]-data[5])

    # bin, int, cont (second-order)
    if data[7] != 0:
      column[7] = str(data[7])
    if data[8]-data[7] != 0:
      column[8] = str(data[8]-data[7])
    if data[9]-data[8] != 0:
      column[9] = str(data[9]-data[8])

    # lin, so
    if data[6] != 0:
      column[10] = str(data[6])
    if data[10] != 0:
      column[11] = str(data[10])

    # obj
    if data[5] + data[8] == 0:
      res = objstat_continuous(self.statdict[data[0]])
    else:
      res = objstat_integer(self.statdict[data[0]])

    column[12] = res[0].replace('E-','E$-$') + self.objstatmark[res[1]]

    print('\n & '.join(column))
    print('\\\\')

class htmltable:
  def __init__(self, statdict, withcss):
    self.statdict = statdict
    self.withcss = withcss
    self.objstatmark = ['<sup>&ensp;</sup>', '<sup>v</sup>', '<sup>a</sup>']
    self.oddrow = False

  def opentable(self):
    print('''<div>''')

    if self.withcss:
      print('''\
  <style type="text/css" scoped>
  #cblib-stattable
  {
    margin: 45px;
    border-collapse: collapse;
  }
  #cblib-stattable th
  {
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 14px;
    font-weight: normal;
    text-align: center;
    padding: 0px 0px;
    color: #039;
  }
  #cblib-stattable td
  {
    font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
    font-size: 12px;
    font-weight: normal;
    padding: 4px 8px;
    color: #669;
  }
  #cblib-stattable div.nogroup
  {
    padding: 4px 8px;
    padding-top: 2px;
  }
  #cblib-stattable div.group
  {
    padding-bottom: 2px;
  }
  #cblib-stattable div.lborder
  {
    margin-right: 8px;
    border-bottom: 1px dotted black;
  }
  #cblib-stattable div.cborder
  {
    margin: 0px 8px;
    border-bottom: 1px dotted black;
  }
  #cblib-stattable div.rborder
  {
    margin-left: 8px;
    border-bottom: 1px dotted black;
  }
  #cblib-stattable .rsep
  {
    border-right: 1px solid #dde;
  }
  #cblib-stattable .strcol
  {
    text-align: left;
  }
  #cblib-stattable .numcol
  {
    text-align: right;
  }
  #cblib-stattable .oddrow
  {
    background: #e8edff;
  }
  </style>''')

    print('''\
  <table id="cblib-stattable">
    <thead>
      <tr>
        <th colspan="1"><div class="group">&nbsp;</div></th>
        <th colspan="3"><div class="group">&nbsp;</div></th>
        <th colspan="6"><div class="group">Conic&nbsp;domain&nbsp;entries</div></th>
        <th colspan="2"><div class="group">&nbsp;</div></th>
        <th colspan="1"><div class="group">&nbsp;</div></th>
      </tr>
      <tr>
        <th colspan="1"><div class="group">&nbsp;</div></th>
        <th colspan="3"><div class="group lborder">Size</div></th>
        <th colspan="3"><div class="group cborder">Linear</div></th>
        <th colspan="3"><div class="group cborder">Second-order</div></th>
        <th colspan="2"><div class="group cborder">Conic&nbsp;domains</div></th>
        <th colspan="1"><div class="group rborder">Objective</div></th>
      </tr>''')

    column = ['Instances','var','map','nnz','bin','int','cont','bin','int','cont','lin','so','&nbsp;']
    out = ''
    for i in range(0,13):
      out += '<th><div class="nogroup ' + self.__colclass(i, True) + '">' + str(column[i]) + '</div></th>'
    print(self.__row(out))

    print('''\
    </thead>
    <tfoot>
      <tr><td colspan="13"><br/><sup>a(ccurancy)</sup> Infeasibility meassures exceed 10<sup>-4</sup> on some primitive cones or integer requirements (points not normalized).</td></tr>
      <tr><td colspan="13"><sup>v(alue)</sup> Objective neither claimed by a solver (mixed-integer case) nor certified by a feasible dual solution (continuous case), <br/>to be within an absolute gap of 10<sup>-4</sup> or relative gap of 10<sup>-7</sup> from optimality.</td></tr>
    </tfoot>
    <tbody>''')

  def closetable(self):
    print('''\
    </tbody>
  </table>
</div>''')

  def addrow(self, data):
    self.oddrow = not self.oddrow

    column = ['']*13

    # Name
    column[0] = data[0]

    # var, map, nnz
    column[1] = data[1]
    column[2] = data[2]
    column[3] = data[3]

    # bin, int, cont (linear)
    if data[4] != 0:
      column[4] = data[4]
    if data[5]-data[4] != 0:
      column[5] = data[5]-data[4]
    if data[6]-data[5] != 0:
      column[6] = data[6]-data[5]

    # bin, int, cont (second-order)
    if data[7] != 0:
      column[7] = data[7]
    if data[8]-data[7] != 0:
      column[8] = data[8]-data[7]
    if data[9]-data[8] != 0:
      column[9] = data[9]-data[8]

    # lin, so
    if data[6] != 0:
      column[10] = data[6]
    if data[10] != 0:
      column[11] = data[10]

    # obj
    if data[5] + data[8] == 0:
      res = objstat_continuous(self.statdict[data[0]])
    else:
      res = objstat_integer(self.statdict[data[0]])

    column[12] = res[0].replace('E-','E&minus;') + self.objstatmark[res[1]]

    out = ''
    for i in range(0,13):
      out += '<td class="' + self.__colclass(i, False) + '">' + str(column[i]) + '</td>'
    print(self.__row(out))

  def __colclass(self, idx, isheader):
    postfix = ''
    if not isheader and idx in [0,3,9,11]:
      postfix = ' rsep'

    if idx == 0:
      return('strcol' + postfix)
    else:
      return('numcol' + postfix)

  def __row(self, value):
    if self.oddrow:
      return('      <tr class="oddrow">' + value + '</tr>')
    else:
      return('      <tr>' + value + '</tr>')

def objstat_continuous(csvstat):
  if csvstat['perr'] == '' or csvstat['derr'] == '':
    return('?')

  perr = float(csvstat['perr'])
  pobj = float(csvstat['pobj'])
  derr = float(csvstat['derr'])
  dobj = float(csvstat['dobj'])

  # OPTIMAL
  if csvstat['pcer'] == 'FEASIBILITY' and csvstat['dcer'] == 'FEASIBILITY' and \
     perr <= 1e-4 and derr <= 1e-4 and ( \
       abs(pobj - dobj) <= 1e-4 or \
       abs(pobj - dobj) / max(1, abs(pobj), abs(dobj)) <= 1e-7 \
     ):
    return(('{:.4E}'.format(float(csvstat['pobj'])), 0))

  # PRIMAL INFEASIBLE
  if csvstat['pcer'] == 'INFEASIBILITY' and perr <= 1e-4:
    return(('Primal infeasible', 0))

  # DUAL INFEASIBLE
  if derr <= 1e-4 and csvstat['dcer'] == 'INFEASIBILITY':
    return(('Dual infeasible', 0))

  # PRIMAL FEASIBLE
  if csvstat['pcer'] == 'FEASIBILITY' and perr <= 1e-4:
    return(('{:.4E}'.format(float(csvstat['pobj'])), 1))

  # BEST GUESS
  if perr <= derr or csvstat['dcer'] == 'FEASIBILITY':
    if csvstat['pcer'] == 'FEASIBILITY':
      return(('{:.4E}'.format(float(csvstat['pobj'])), 2))
    else:
      return(('Primal infeasible', 2))
  else:
    return(('Dual infeasible', 2))


def objstat_integer(csvstat):
  if csvstat['perr'] == '':
    return('?')

  perr = float(csvstat['perr'])

  # OPTIMAL
  if csvstat['pcer'] == 'FEASIBILITY' and perr <= 1e-4 and csvstat['claim'] == 'INTEGER_OPTIMALITY':
    return(('{:.4E}'.format(float(csvstat['pobj'])), 0))

  # INFEASIBLE
  if (csvstat['pcer'] == 'INFEASIBILITY' and perr <= 1e-4) or csvstat['claim'] == 'INTEGER_INFEASIBILITY':
    return(('Infeasible', 0))

  # UNBOUNDED IF INFEASIBLE
  if not csvstat['derr'] == '':
    derr = float(csvstat['derr'])

    if derr <= 1e-4 and csvstat['dcer'] == 'INFEASIBILITY':
      return(('Unbound if feasible', 0))

  # FEASIBLE
  if csvstat['pcer'] == 'FEASIBILITY' and perr <= 1e-4:
    return(('{:.4E}'.format(float(csvstat['pobj'])), 1))

  # BEST GUESS
  if csvstat['derr'] == '' or perr <= derr or csvstat['dcer'] == 'FEASIBILITY':
    if csvstat['pcer'] == 'FEASIBILITY':
      return(('{:.4E}'.format(float(csvstat['pobj'])), 2))
    else:
      return(('Infeasible', 2))
  else:
    return(('Unbound if feasible', 2))


def stattable(backend, filtexpr):
  # Find the directory of this script
  scriptdir = os.path.split(inspect.getfile( inspect.currentframe() ))[0]
  rootdir = os.path.join(scriptdir,'..','..')

  # Read instance status data
  csvfile = open(os.path.join(rootdir,'instances','stat.csv'), 'rt')
  csvreader = csv.reader(csvfile, delimiter=';', quotechar='"')
  next(csvreader)
  statdict = dict((rows[0], dict((x,y) for x,y in zip(statdictnames, rows[1:]))) for rows in csvreader)
  csvfile.close()

  # Select backend
  out = None
  if backend == "latex":
    out = latextable(statdict)
  elif backend == "html":
    out = htmltable(statdict, False)
  elif backend == "html+css":
    out = htmltable(statdict, True)
  else:
    raise Exception('Backend not recognized!')

  # Print table
  out.opentable()
  filter(filtexpr, "[||name||, ||var||, ||map||, ||nnz||, ||binary|lin||, ||int|lin||, ||entries|lin||, ||binary|so||, ||int|so||, ||entries|so||, ||cones|so||]", out.addrow)
  out.closetable()


if __name__ == "__main__":
  filtexpr = ""

  try:
    # Verify command line arguments
    if len(sys.argv) != 2 and len(sys.argv) != 3:
      raise Exception(''.join([
          'Incorrect usage, try all instances in LaTeX format:', '\n',
          '  python ', sys.argv[0], ' latex']))

    if len(sys.argv) >= 3:
      filtexpr = sys.argv[2]

    # Print statistics table
    stattable(sys.argv[1], filtexpr)

  except Exception as e:
    print(str(e))
