#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Get testbed configuration
# Copyright (C) 2014-2024 by Thomas Dreibholz
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#
# Contact: dreibh@simula.no

import os
import sys
import math
import urllib.request, urllib.error, urllib.parse
import subprocess
import re

# NorNet
from NorNetSiteSetup import *;
from NorNetTools     import *;
from NorNetAPI       import *;


# ###### Find tag in style string ###########################################
def findTag(style, tagName, default = None):
   s1 = style.split(';')
   for r in s1:
      s2 = r.split(':')
      try:
         if s2[0].strip() == tagName:
            return  s2[1].strip()
      except:
         pass
   return default


def rreplace(s, old, new, count):
   return (s[::-1].replace(old[::-1], new[::-1], count))[::-1]



class TableOutput:
   def __init__(self, useHyperlinks):
      self.useHpyerlinks = useHpyerlinks
      self.indent        = 0

      self.write('<?xml version="1.0" encoding="utf-8" ?>\n')
      self.write('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
      self.write('<html>\n')
      self.write('<head>\n')
      self.write('<title>The NorNet Core Site List</title>\n')
      self.write('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n')
      self.write('<link rel="stylesheet" href="essen.css" type="text/css" />\n')
      self.write('<link rel="shortcut icon" href="https://www.nntb.no/favicon.ico" />\n')
      self.write('<style type="text/css">\n')
      self.write('td {\n')
      self.write(' text-align: center;\n')
      self.write(' vertical-align: middle;\n')
      self.write('}\n')
      self.write('</style>\n')
      self.write('</head>\n')
      self.write('<body>\n')

   def __del__(self):
      self.write('</body>\n</html>\n')

   def write(self, text):
      sys.stdout.write(text)

   def comment(self, text):
      self.write('<!-- ' + text + ' -->\n')

   def header(self, level, label, text):
      self.write('<h' + str(level) + ' id="' + label + '">' + text + '</h' + str(level) + '>\n')

   def beginEnumeration(self):
      self.write('<ol>\n')

   def endEnumeration(self):
      self.write('</ol>\n')

   def beginList(self):
      self.write('<ul>\n')

   def endList(self):
      self.write('</ul>\n')

   def beginItem(self):
      self.write(' <li>')

   def endItem(self):
      self.write('</li>\n')

   def makeItem(self, item):
      self.beginItem()
      self.write(item)
      self.endItem()

   def beginTable(self, columns=2, textColor=None):
      self.doIndentation(True)
      options = 'border="1"'
      if textColor != None:
         options = options + ' style="color: ' + textColor + ';"'
      self.write('<table ' + options + '>\n')
      self.indent = self.indent + 1

   def endTable(self):
      self.indent = self.indent - 1
      self.write('</table>\n')

   def beginTableFirstHead(self):
      return

   def endTableFirstHead(self):
      return

   def beginTableHead(self):
      return False

   def endTableHead(self):
      return

   def beginRow(self):
      self.doIndentation(False)
      self.write('<tr>')

   def endRow(self):
      self.write('</tr>\n')

   def beginCell(self, colspan = -1, rowspan = -1, style = ''):
      opts=''
      if colspan >= 0:
         opts = opts + ' colspan="' + str(colspan) + '"'
      if rowspan >= 0:
         opts = opts + ' rowspan="' + str(rowspan) + '"'
      if style != '':
         opts = opts + ' style="' + style + '"'
      self.write('<td' + opts + '>')

   def endCell(self):
      self.write('</td>')

   def multiRowFill(self, colspan = -1):
      return

   def makeBold(self, text):
      return '<strong>' + text + '</strong>'

   def makeTiny(self, text):
      return '<small>' + text + '</small>'

   def cell(self, cell = '', colspan = -1, rowspan = -1, style = ''):
      self.beginCell(colspan, rowspan, style)
      self.write(cell)
      self.endCell()

   def makeXref(self, xref, label):
      if self.useHpyerlinks == True:
         return '<a href="#' + xref + '">' + label + '</a>'
      else:
         return label

   def makeURL(self, url, label):
      if self.useHpyerlinks == True:
         return '<a href="' + url + '">' + label + '</a>'
      else:
         return label

   def makeImage(self, url, style):
      opts=''
      if style != '':
         opts = opts + ' style="' + style + '"'
      return '<img src="' + url + '"' + opts + ' alt="" />'

   def makeParagraph(self, text):
      return '<p>' + text + '</p>\n'

   def makeText(self, text):
      return text

   def makeSub(self, text):
      return '<sub>' + text + '</sub>'

   def makeSup(self, text):
      return '<sup>' + text + '</sup>'

   def makeSmiley(self, positive = True):
      if positive == True:
         return '☺'
      else:
         return '☹'

   def hasTableOfContents(self):
      return False

   def addToIndex(self, item, referTo = None, addToGlossary = False):
      return ''

   def doIndentation(self, newline = False):
      if newline == True:
         self.write('\n')
      i = self.indent
      while i > 0:
         self.write(' ')
         i = i - 1


class LaTeXTableOutput(TableOutput):
   def __init__(self, useHyperlinks, dataDirectory = ''):
      self.useHpyerlinks = useHpyerlinks
      self.dataDirectory = dataDirectory
      self.indent        = 0
      self.glossary      = []
      self.write('\\documentclass[11pt,a4paper,norsk,german,english,titlepage]{book}\n')
      self.write('\\usepackage{babel}\n')
      self.write('\\usepackage{fixltx2e}\n')
      self.write('\\usepackage{a4wide}\n')
      self.write('\\usepackage[T2A]{fontenc}\n')
      self.write('\\usepackage[utf8]{inputenc}\n')
      self.write('\\usepackage{graphicx}\n')
      self.write('\\usepackage{longtable}\n')
      self.write('\\usepackage{multirow}\n')
      self.write('% ====== Page-break fix for \cline ========================\n')
      self.write('\\makeatletter\n')
      self.write('\\def\\@cline#1-#2\\@nil{%\n')
      self.write('\\omit\n')
      self.write('\\@multicnt#1%\n')
      self.write('\\advance\\@multispan\\m@ne\n')
      self.write('\\ifnum\\@multicnt=\\@ne\\@firstofone{&\\omit}\\fi\n')
      self.write('\\@multicnt#2%\n')
      self.write('\\advance\\@multicnt-#1%\n')
      self.write('\\advance\\@multispan\@ne\n')
      self.write('\\leaders\\hrule\\@height\\arrayrulewidth\\hfill\n')
      self.write('\\cr\n')
      self.write('\\noalign{\\nobreak\\vskip-\\arrayrulewidth}}\n')
      self.write('\\makeatother\n')
      self.write('% =========================================================\n')
      self.write('\\usepackage{textcomp}\n')
      self.write('\\usepackage{wasysym}\n')
      self.write('\\usepackage{url}\n')
      self.write('\\urlstyle{rm}\n')
      self.write('\\usepackage{makeidx}\n')
      self.write('\\makeindex\n')
      self.write('\\usepackage[dvipsnames]{xcolor}\n')
      self.write('\\definecolor{darkred}{rgb}{0.5,0,0}\n')
      self.write('\\definecolor{darkgreen}{rgb}{0,0.5,0}\n')
      self.write('\\definecolor{darkblue}{rgb}{0,0,0.5}\n')
      self.write('\\definecolor{black}{rgb}{0,0,0}\n')
      self.write('\\usepackage[draft=false,pdfpagemode=UseOutlines,plainpages=false,hypertexnames=true,pdfpagelabels=true,hyperindex=true,colorlinks=true]{hyperref}\n')
      self.write('\\hypersetup{colorlinks,pdfauthor={Priv.-Doz.\ Dr.\ Thomas Dreibholz},pdftitle={The NorNet Core Configuration},pdfsubject={NorNet Core},pdfkeywords={NorNet Core, Testbed, Configuration},linkcolor=darkblue,urlcolor=blue,citecolor=darkblue,filecolor=darkgreen}\n')
      self.write('\\usepackage{nomencl}\n')
      self.write('\\renewcommand{\\nomname}{Glossary}\n')
      self.write('\\setlength{\\nomitemsep}{-\parsep}\n')
      self.write('\\setlength{\\nomlabelwidth}{4.5cm}\n')
      self.write('\\makenomenclature\n')
      self.write('\\begin{document}\n')
      self.write('\\tableofcontents\\cleardoublepage\n')
      self.write('\\printnomenclature\n')
      self.write('\\addcontentsline{toc}{chapter}{Glossary}\\cleardoublepage\n')

   def __del__(self):
      self.write('\\printindex{}\n')
      self.write('\\pagebreak\n')
      self.write('\\cleardoublepage\n')
      self.write('\\end{document}\n')

   def comment(self, text):
      self.write('% ' + text + '\n')

   def header(self, level, label, text):
      if level == 1:
         c = '@' ; s = 4 ; cmd = '\\chapter{' + text + '}'
      elif level == 2:
         c = '#' ; s = 3 ; cmd = '\\section{' + text + '}'
      elif level == 3:
         c = '=' ; s = 2 ; cmd = '\\subsection{' + text + '}'
      else:
         c = '-' ; s = 1 ; cmd = '\\subsubsection{' + text + '}'

      self.write('\n' * s + '% ' + 76 * c + '\n' + \
                 cmd + '\n' + \
                 '\\label{' + label + '}\n' + \
                 '% ' + 76 * c + '\n\n')

   def beginList(self):
      self.write('\\begin{itemize}\n')

   def endList(self):
      self.write('\\end{itemize}\n')

   def beginEnumeration(self):
      self.write('\\begin{enumerate}\n')

   def endEnumeration(self):
      self.write('\\end{enumerate}\n')

   def beginItem(self):
      self.write(' \item ')

   def endItem(self):
      self.write('\n')

   def beginTable(self, columns=2, textColor=None):
      self.write('\n% ' + 76 * '%' + '\n')
      self.write('\\begin{small}\n')
      self.write('\\begin{center}\n')
      self.write('\\begin{longtable}{|' + 'c|' * columns + '}\n \\hline')
      self.doIndentation(True)
      self.indent = self.indent + 1
      self.totalColumns = columns
      self.rowNumber    = 0
      self.multiRowList = []

   def endTable(self):
      self.indent = self.indent - 1
      self.write('\\end{longtable}\n')
      self.write('\\end{center}\n')
      self.write('\\end{small}\n')
      self.write('% ' + 76 * '%' + '\n')

   def beginTableFirstHead(self):
      return

   def endTableFirstHead(self):
      self.write('\\endfirsthead\n')

   def beginTableHead(self):
      self.write('\\hline\n')
      return True

   def endTableHead(self):
      self.write('\\endhead\n')

   def beginRow(self):
      self.clineArray = []
      self.doIndentation(False)
      self.columnNumber = 0
      self.rowNumber    = self.rowNumber + 1

   def endRow(self):
      self.write(' \\\\')
      if len(self.clineArray) == self.columnNumber:
         self.write(' \\hline')
      else:
         self.write('* ')
         for c in self.clineArray:
            self.write('\\cline{' + str(c) + '-' + str(c) + '}')
      self.write('\n')
      if self.columnNumber != self.totalColumns:
         sys.stderr.write('ERROR: cell number ' + str(self.columnNumber) + ' != ' + str(self.totalColumns) + ' at endRow()!\n')
         sys.exit(1)

   def beginCell(self, colspan = -1, rowspan = -1, style = ''):
      self.columnNumber = self.columnNumber + 1
      startColumnNumber = self.columnNumber
      if self.columnNumber > 1:
         self.write(' & ')
      self.colspan = colspan
      self.rowspan = rowspan

      alignmentOption = 'c'
      alignment = findTag(style, 'text-align')
      if alignment == 'left':
         alignmentOption = 'l'
      elif alignment == 'right':
         alignmentOption = 'r'
      width = findTag(style, 'width', '*')

      if ((alignmentOption != 'c') and (self.colspan < 1)):
         self.colspan = 1   # Use \multicolumn for non-centered cell

      if self.colspan > 0:
         if width != '*':
            alignmentOption = 'p{' + width + '}'
         self.write('\\multicolumn{' + str(self.colspan) + '}{|' + alignmentOption + '|}{')
         self.columnNumber = self.columnNumber + (self.colspan - 1)
         cs = self.colspan
      else:
         cs = 1

      if self.rowspan > 1:
         self.write('\\multirow{' + str(self.rowspan) + '}{' + width + '}{')
         self.multiRowList.append([ self.rowNumber + (self.rowspan - 1), self.columnNumber - cs])
      else:
         for c in range(startColumnNumber, self.columnNumber + 1):
            self.clineArray.append(c)

   def endCell(self):
      if self.rowspan > 1:
         self.write('}')
      if self.colspan > 0:
         self.write('}')

   def multiRowFill(self, colspan = -1):
      isEndOfBlock = False
      for entry in self.multiRowList:
         if ((entry[0] == self.rowNumber) and (entry[1] == self.columnNumber)):
            isEndOfBlock = True
            self.multiRowList.remove(entry)
            break

      bak = list(self.clineArray)
      self.beginCell(colspan)
      self.endCell()
      if not isEndOfBlock:
         self.clineArray = bak

   def makeBold(self, text):
      return '\\textbf{' + text + '}'

   def makeTiny(self, text):
      return '\\tiny{' + text + '}'

   def makeXref(self, xref, label):
      if self.useHpyerlinks == True:
         return '\hyperref[' + xref + ']{' + label + '}'
      else:
         return label

   def makeURL(self, url, label):
      if self.useHpyerlinks == True:
         if url == label:
            return '\\url{' + url + '}'
         else:
            return '\href{' + url + '}{' + label + '}'
      else:
         return label

   def makeImage(self, url, style):
      name      = self.dataDirectory + '/' + makeNameFromUnicode(os.path.basename(url), False)['ascii'].replace(' ', '_', 1000000000)
      extension = os.path.splitext(name)[1]
      url       = urllib.parse.quote(url.encode('utf-8')).replace('https%3A', 'https:', 1)

      if not os.path.exists(name):
         sys.stderr.write('Download: ' + url + ' -> ' + name + ' ...\n')
         try:
            image = urllib.request.urlopen(url)
         except:
            sys.stderr.write('ERROR: Failed to download ' + url + '!\n')
            return ''
         output = open(name, 'wb')
         output.write(image.read())
         output.close()

      if extension == '.svg':
         oldName = name
         name    = rreplace(name, '.svg', '.pdf', 1)
         if not os.path.exists(name):
            sys.stderr.write('Convert: ' + oldName + ' -> ' + name + ' ...\n')
            subprocess.call(['/usr/bin/inkscape', '-f', oldName, '-A', name])

      options = 'keepaspectratio'
      width = findTag(style, 'width')
      if width != None:
         options = options + ',width=' + width
      height = findTag(style, 'height')
      if height != None:
         options = options + ',height=' + height

      return '\\includegraphics[' + options + ']{' + name + '}'


   def makeText(self, text):
      return str.replace(text, '_', '\\_', 1000000000)

   def makeParagraph(self, text):
      return text

   def makeSmiley(self, positive = True):
      if positive == True:
         return '\\smiley{}'
      else:
         return '\\frownie{}'

   def hasTableOfContents(self):
      return True

   def addToIndex(self, item, referTo = None, addToGlossary = False):
      if ((referTo != None) and (referTo != item)):
         if not item in self.glossary:
            self.glossary.append(item)
            if addToGlossary == True:
               return '\\nomenclature{' + item + '}{' + referTo + '}\\index{' + item + '|see{' + referTo + '}}'
            else:
               return '\\index{' + item + '|see{' + referTo + '}}'
      return '\index{' + item + '}'

   def makeSub(self, text):
      return '\\raisebox{-1ex}{' + text + '}'

   def makeSup(self, text):
      return '\\textsuperscript{' + text + '}'



# ###### Get NorNet node object for sliver ##################################
def makePseudoNode(nodeName, site, nodeIndex, model):
   dnsName = makeNameFromUnicode(nodeName)
   norNetNode = {
      'node_site_id'          : site['site_id'],
      'node_index'            : int(nodeIndex),
      'node_name'             : str.lower(dnsName['ascii'])    + '.' + site['site_domain'],
      'node_utf8'             : str.lower(dnsName['utf8']) + '.' + site['site_domain'],
      'node_nornet_interface' : None,
      'node_model'            : model,
      'node_state'            : 'SLIVER',
      'node_tags'             : []
   }
   return norNetNode


# ###### Reserve address ####################################################
addressList = []
def reserve(address):
   if address in addressList:
      error('Address ' + str(address) + ' is already reserved!')
   addressList.append(address)


# ###### Extract IPs of a node or sliver ####################################
def extractIPs(siteIndex, nodeIndex, sliceNodeIndex = 0):
   results = [ ]
   for onlyDefault in [ True, False ]:
      for providerIndex in providerList:
         if ( ((onlyDefault == True) and (providerIndex == site['site_default_provider_index'])) or \
               ((onlyDefault == False) and (providerIndex != site['site_default_provider_index'])) ):

            provider = providerList[providerIndex]
            ifIPv4   = makeNorNetIP(providerIndex, siteIndex, nodeIndex, 4, sliceNodeIndex).ip
            ifIPv6   = makeNorNetIP(providerIndex, siteIndex, nodeIndex, 6, sliceNodeIndex).ip
            reserve(ifIPv4)
            reserve(ifIPv6)

            providerURL = ''
            try:
               providerURL = NorNet_ProviderList[providerIndex][2]
            except:
               pass
            providerLabel = output.makeURL(providerURL, provider['provider_short_name']) + ' (' + str(provider['provider_index']) + ')'

            if showAddresses == True:
               results.append([ providerLabel, str(ifIPv4), str(ifIPv6) ])
            else:
               externalNetworkIPv4 = IPv4Address(str(provider['provider_tunnelbox_ipv4'].ip))
               if externalNetworkIPv4 != IPv4Address('0.0.0.0'):
                  labelIPv4 = 'native'
               else:
                  labelIPv4 = 'tunnelled'
               externalNetworkIPv6 = IPv6Address(str(provider['provider_tunnelbox_ipv6'].ip))
               if externalNetworkIPv6 != IPv6Address('::'):
                  labelIPv6 = 'native'
               else:
                  labelIPv6 = 'tunnelled'
               results.append([ providerLabel, labelIPv4, labelIPv6 ])

   return results



# ###### Main program #######################################################
siteImageURL  = 'https://www.nntb.no/Artwork/Sites/Small'
siteFlagURL   = 'https://www.nntb.no/Artwork/Graphics/Flags'

showAddresses     = True
showSlivers       = True
showContacts      = True
useHpyerlinks     = True
outputFormat      = 'html'
headerBaseLevel   = 2
dataDirectory     = 'NorNet-Configuration-Data'
i = 1
while i < len(sys.argv):
   match = re.search('^-([a-z]*)=(.*)', sys.argv[i])
   if match != None:
      if match.group(1) == 'directory':
         dataDirectory = match.group(2)
      else:
        error('Invalid option parameter: ' + sys.argv[i])
   elif sys.argv[i] == '-noaddresses':
      showAddresses = False
   elif sys.argv[i] == '-noslivers':
      showSlivers = False
   elif sys.argv[i] == '-nocontacts':
      showContacts = False
   elif sys.argv[i] == '-nohyperlinks':
      useHpyerlinks = False
   elif sys.argv[i] == '-html':
      outputFormat = 'html'
   elif sys.argv[i] == '-latex':
      outputFormat      = 'latex'
      headerBaseLevel   = 1
   else:
      error('Usage: ' + sys.argv[0] + ' [-directory=data_directory] [-noaddresses] [-nocontacts] [-noslivers] [-nohyperlinks] [-html|-latex]')
   i = i + 1

loginToPLC(quietMode = True)

siteList  = fetchNorNetSite(None, False)
nodeList  = fetchNorNetNode(None)
sliceList = fetchNorNetSlice(None)


# ====== Handle slices ======================================================
if sliceList == None:
   print('ERROR: Slice list is empty?! Missing NorNet patch?')
   sys.exit(1)

sliverNodeList = []
for slice in sliceList:
   for node in nodeList:
      if node['node_id'] in slice['slice_node_ids']:
         sliceDNSName   = str.replace(slice['slice_name'], '_', '-')
         sliceHostname  = sliceDNSName + '.' + getHostnameFromFQDN(node['node_utf8'])
         sliceNodeIndex = getSliceNodeIndexOfNorNetSlice(slice, node)
         sliceNode = makePseudoNode(sliceHostname,
                                    getNorNetSiteOfNode(siteList, node),
                                    node['node_index'],
                                    node['node_model'])
         # Add a pseudo entry to create a slice address later
         sliceNode['pseudo_slice_node_index'] = getSliceNodeIndexOfNorNetSlice(slice, node)
         sliverNodeList.append(sliceNode)

nodeList       = sorted(nodeList, key = lambda x: x['node_index'])
sliverNodeList = sorted(sliverNodeList, key = lambda x: x['pseudo_slice_node_index'])

if outputFormat == 'latex':
   if not os.path.exists(dataDirectory):
      os.makedirs(dataDirectory)
   output = LaTeXTableOutput(useHpyerlinks, dataDirectory)
else:
   output = TableOutput(useHpyerlinks)
siteIndexList = []
for siteIndex in siteList:
   siteIndexList.append(siteIndex)


# ====== Make index =========================================================
output.comment('BEGIN-OF-SITES-BLOCK')

if output.hasTableOfContents() == False:
   output.header(headerBaseLevel, 'cha:Index', 'Index')
   output.beginList()
   output.makeItem(output.makeXref('cha:Sites',      'Site Table'))
   output.makeItem(output.makeXref('cha:Providers',  'Provider Table'))
   output.makeItem(output.makeXref('cha:Site-Statistics', 'Site Statistics'))
   output.makeItem(output.makeXref('cha:Images',     'Site Images'))
   output.beginItem()
   output.write(output.makeXref('cha:Site-Details', 'Site Details'))
   output.beginList()
   for siteIndex in sorted(siteIndexList):
      site = siteList[siteIndex]
      output.makeItem(output.makeXref('sec:' + site['site_short_name'], site['site_utf8']))
   output.endList()
   output.endItem()
   if showSlivers == True:
      output.beginItem()
      output.write(output.makeXref('cha:Slice-Details', 'Slice Details'))
      output.endItem()
   output.endList()


# ====== Get site table =====================================================
output.header(headerBaseLevel, 'cha:Sites', 'Site Table')

maxISPs = 0
for siteIndex in sorted(siteIndexList):
   providerList = getNorNetProvidersForSite(siteList[siteIndex])
   maxISPs = max(maxISPs, len(providerList))

output.beginTable(2 + maxISPs)
output.beginRow()
output.cell('Index')
output.cell('Site', -1, -1, 'width: 20em;')
for i in range(1, maxISPs + 1):
   output.cell('ISP ' + str(i), -1, -1, 'width: 16em;')
output.endRow()

activeSites          = 0
numInterfacesIPv4    = 0
numInterfacesIPv6    = 0
activeSitesISPs      = set([])
activeSitesCountries = set([])
for siteIndex in sorted(siteIndexList):
   site         = siteList[siteIndex]
   siteTagsList = site['site_tags']
   siteName     = site['site_utf8']
   siteEnabled  = site['site_enabled']
   providerList = getNorNetProvidersForSite(site)

   # ------ Statistics ------------------------------------------------------
   if siteEnabled == True:
      activeSites = activeSites + 1
      for siteTag in siteTagsList:
         if siteTag['tagname'] == 'nornet_site_country_code':
            activeSitesCountries.add(siteTag['value'])


   # ------ Get interface information ---------------------------------------
   output.beginRow()
   output.cell(str(siteIndex))
   cellContent = site['site_utf8']
   output.cell(output.addToIndex(site['site_utf8']) + output.addToIndex('Site!' + site['site_utf8']) + output.makeXref('sec:' + site['site_short_name'], cellContent), -1, -1, 'text-align: left;')
   isp = 0
   for onlyDefault in [ True, False ]:
      for providerIndex in providerList:
         if ( ((onlyDefault == True)  and (providerIndex == site['site_default_provider_index'])) or \
               ((onlyDefault == False) and (providerIndex != site['site_default_provider_index'])) ):
            provider = providerList[providerIndex]
            providerURL = ''
            try:
               providerURL = NorNet_ProviderList[providerIndex][2]
            except:
               pass
            cellContent = provider['provider_short_name']
            cellContent = output.makeURL(providerURL, cellContent)

            externalNetworkIPv6 = IPv6Address(str(provider['provider_tunnelbox_ipv6'].ip))
            if externalNetworkIPv6 != IPv6Address('::'):
               cellContent = cellContent + output.makeSup('6')

            # ------ Statistics ---------------------------------------------
            if siteEnabled == True:
               if siteEnabled == True:
                  activeSitesISPs.add(providerIndex)

               externalNetworkIPv4 = IPv4Address(str(provider['provider_tunnelbox_ipv4'].ip))
               if externalNetworkIPv4 != IPv4Address('0.0.0.0'):
                  numInterfacesIPv4 = numInterfacesIPv4 + 1

               if externalNetworkIPv6 != IPv6Address('::'):
                  numInterfacesIPv6 = numInterfacesIPv6 + 1

            output.cell(cellContent)
            isp = isp + 1

   for i in range(isp + 1, maxISPs + 1):
      output.cell('–')

   output.endRow()

output.endTable()


# ====== Get ISP table ======================================================
output.header(headerBaseLevel, 'cha:Providers', 'Provider Table')

output.beginTable(4)
output.beginRow()
output.cell('Index')
output.cell('Short Name', -1, -1, 'text-align: left;')
output.cell('Long Name',  -1, -1, 'text-align: left;')
output.cell('URL',        -1, -1, 'text-align: left;')
output.endRow()

for providerIndex in sorted(activeSitesISPs):
   providerLongName  = '???'
   providerShortName = '???'
   providerURL       = ''
   try:
      providerLongName  = NorNet_ProviderList[providerIndex][0]
      providerShortName = NorNet_ProviderList[providerIndex][1]
      providerURL       = NorNet_ProviderList[providerIndex][2]
   except:
      pass

   output.beginRow()
   output.cell(str(providerIndex))
   output.cell(output.addToIndex(providerShortName, providerLongName) + output.addToIndex('Provider!' + providerLongName) + output.makeURL(providerURL, providerShortName), -1, -1, 'text-align: left;')
   output.cell(output.addToIndex(providerLongName)                    + output.makeURL(providerURL, providerLongName),  -1, -1, 'text-align: left;')
   output.cell(output.makeTiny(output.makeURL(providerURL,providerURL)), -1, -1, 'text-align: left;')
   output.endRow()

output.endTable()


# ====== Get site statistics ================================================
output.header(headerBaseLevel, 'cha:Site-Statistics', 'Site Statistics')
output.beginTable()

output.beginRow()
output.cell('Active Sites', -1, -1, 'text-align: left;')
output.cell(str(activeSites))
output.endRow()

output.beginRow()
output.cell('Distinct ISPs of Active Sites', -1, -1, 'text-align: left;')
output.cell(str(len(activeSitesISPs)))
output.endRow()

output.beginRow()
output.cell('Distinct Countries of Active Sites', -1, -1, 'text-align: left;')
output.cell(str(len(activeSitesCountries)))
output.endRow()

output.beginRow()
output.cell('Total IPv4 Interfaces', -1, -1, 'text-align: left;')
output.cell(str(numInterfacesIPv4))
output.endRow()

output.beginRow()
output.cell('Total IPv4 Tunnels', -1, -1, 'text-align: left;')
output.cell(str(int(numInterfacesIPv4 * (numInterfacesIPv4 -1) / 2)))
output.endRow()

output.beginRow()
output.cell('Total IPv6 Interfaces', -1, -1, 'text-align: left;')
output.cell(str(numInterfacesIPv6))
output.endRow()

output.beginRow()
output.cell('Total IPv6 Tunnels', -1, -1, 'text-align: left;')
output.cell(str(int(numInterfacesIPv6 * (numInterfacesIPv6 - 1) / 2)))
output.endRow()

output.beginRow()
output.cell('Inactive Sites', -1, -1, 'text-align: left;')
output.cell(str(len(siteIndexList) - activeSites))
output.endRow()

output.endTable()


# ====== Get site images ====================================================
output.header(headerBaseLevel, 'cha:Images', 'Site Images')

columns = int(math.floor(math.sqrt(len(siteIndexList))))
output.beginTable(columns)
siteNumber = 0
inRow     = False

for siteIndex in sorted(siteIndexList):
   site = siteList[siteIndex]
   if (siteNumber % columns) == 0:
      if inRow == True:
         output.endRow()
      output.beginRow()
      inRow = True
   imageURL = siteImageURL + '/' + site['site_utf8'] + '.jpeg'
   siteNumber = siteNumber + 1
   output.cell(output.makeXref('sec:' + site['site_short_name'], output.makeImage(imageURL, 'width: 9em; height: 6em; overflow: hidden;')))

if inRow == True:
   i = len(siteIndexList)
   while (i % columns) != 0:
      i = i + 1
      output.cell()

   output.endRow()

output.endTable()


# ====== Get site details ===================================================
output.header(headerBaseLevel, 'cha:Site-Details', 'Site Details')
for siteIndex in sorted(siteIndexList):

   # ====== Get site information ============================================
   site = siteList[siteIndex]
   siteName        = site['site_utf8']
   providerList    = getNorNetProvidersForSite(site)
   siteTagsList    = site['site_tags']
   siteEnabled     = site['site_enabled']
   siteCountryCode = ''
   siteCountry     = ''
   siteProvince    = ''
   siteCity        = ''

   for siteTag in siteTagsList:
      if siteTag['tagname'] == 'nornet_site_country_code':
         siteCountryCode = siteTag['value']
      elif siteTag['tagname'] == 'nornet_site_country':
         siteCountry = siteTag['value']
      elif siteTag['tagname'] == 'nornet_site_province':
         siteProvince = siteTag['value']
      elif siteTag['tagname'] == 'nornet_site_city':
         siteCity = siteTag['value']

   siteLocation = siteCity
   if siteProvince != '':
      siteLocation = siteLocation + ', ' + siteProvince
   siteLocation = siteLocation + '/' + siteCountry


   sitePosition = str(abs(site['site_latitude']))
   if site['site_latitude'] >= 0:
      sitePosition = sitePosition + '°N'
   else:
      sitePosition = sitePosition + '°S'

   sitePosition = sitePosition + ' / ' + str(abs(site['site_longitude']))
   if site['site_longitude'] >= 0:
      sitePosition = sitePosition + '°E'
   else:
      sitePosition = sitePosition + '°W'

   sitePosition = sitePosition + ' / ' + str(site['site_altitude']) + 'm'

   output.header(headerBaseLevel + 1, 'sec:' + site['site_short_name'], siteName + ' (' + str(siteIndex) + ')')


   # ====== Contacts ===========================================================
   if showContacts == True:
      contacts = site['site_contacts']
      if len(contacts) > 0:
         output.write(output.makeParagraph('Contacts:'))
         output.beginEnumeration()
         for contact in contacts:
            found = re.match(r'(.*): ([a-zA-Z0-9\.]+@[a-zA-Z0-9\.-]+)', contact)
            if found:
               contactMail       = found.group(2)
               contactFullName   = found.group(1)
               contactFamilyName = ''
               contactGivenName  = ''

               output.beginItem()
               found = re.match(r'(.*), (.*)', contactFullName)
               if found:
                  contactFamilyName = found.group(1)
                  contactGivenName  = found.group(2)
                  output.write(output.addToIndex(contactFamilyName + ', ' + contactGivenName) + output.makeURL('mailto:' + contactMail, contactGivenName + ' ' + contactFamilyName + ' (' + contactMail + ')'))
               else:
                  output.write(output.addToIndex(contactFullName) + output.makeURL('mailto:' + contactMail, contactFullName + ' (' + contactMail + ')'))

               output.endItem()
         output.endEnumeration()


   # ====== Display site information ========================================
   if showSlivers == True:
      nodePlusSliverCells = 4
   else:
      nodePlusSliverCells = 2

   if siteEnabled:
      output.beginTable(nodePlusSliverCells + 4)
   else:
      output.beginTable(nodePlusSliverCells + 4, 'grey')

   siteInfoTable = [
      [ 'City',         output.addToIndex(siteCity)     + siteCity     ],
      [ 'Province',     output.addToIndex(siteProvince) + output.makeImage(siteFlagURL + '/' + siteCountryCode + '-' + siteProvince + '.svg', 'height: 0.8em;') + siteProvince ],
      [ 'Country',      output.addToIndex(siteCountryCode, siteCountry, True) + output.addToIndex(siteCountry)  + output.makeImage(siteFlagURL + '/Flag-' + siteCountryCode + '.svg', 'height: 0.8em;') + siteCountry + ' (' + siteCountryCode + ')' ],
      [ 'GPS/ГЛОНАСС', sitePosition ],
      [ 'Short Name',   output.addToIndex(site['site_short_name'], siteName, True) + site['site_short_name'] ],
      [ 'Domain',       output.addToIndex(site['site_domain']) + site['site_domain'] ],
      [ 'URL',          output.makeURL(site['site_url'], site['site_url']) ]
   ]

   output.beginTableFirstHead()

   output.beginRow()
   output.cell(output.addToIndex(siteName) + output.makeBold(siteName), nodePlusSliverCells + 4)
   output.endRow()

   firstRow = True
   for siteInfo in siteInfoTable:
      output.beginRow()
      if firstRow == True:
         firstRow = False
         imageURL = siteImageURL + '/' + site['site_utf8'] + '.jpeg'
         output.cell(output.makeImage(imageURL, 'height: 7.75em;'), nodePlusSliverCells, len(siteInfoTable))
      else:
         output.multiRowFill(nodePlusSliverCells)
      output.cell(siteInfo[0], -1, -1, 'text-align: left;')
      output.cell(siteInfo[1], 3, -1, 'text-align: left;')
      output.endRow()


   # ====== Get node information ============================================
   for stage in [1, 2]:

      output.beginRow()
      output.cell('Node', 2, -1, 'width: 8em;')
      if showSlivers == True:
         output.cell('Sliver', 2, -1, 'width: 8em;')
      output.cell('Provider', 2, -1, 'width: 8em;')
      output.cell('IPv4', -1, -1, 'width: 8em;')
      output.cell('IPv6', -1, -1, 'width: 8em;')
      output.endRow()

      if stage == 1:
         output.endTableFirstHead()
         if output.beginTableHead() == False:
            break
         output.beginRow()
         output.cell(output.makeBold(siteName + ' (continued)'), nodePlusSliverCells + 4)
         output.endRow()
      else:
         output.endTableHead()


   # ===== Tunnelbox ========================================================
   isp = 0
   for onlyDefault in [ True, False ]:
      for providerIndex in providerList:
         if ( ((onlyDefault == True)  and (providerIndex == site['site_default_provider_index'])) or \
               ((onlyDefault == False) and (providerIndex != site['site_default_provider_index'])) ):
            isp         = isp + 1
            provider    = providerList[providerIndex]
            providerURL = ''
            try:
               providerURL = NorNet_ProviderList[providerIndex][2]
            except:
               pass
            externalNetworkIPv4 = IPv4Address(str(provider['provider_tunnelbox_ipv4'].ip))
            externalNetworkIPv6 = IPv6Address(str(provider['provider_tunnelbox_ipv6'].ip))
            internalNetworkIPv4 = makeNorNetIP(providerIndex, siteIndex, NorNet_NodeIndex_Tunnelbox, 4).ip
            internalNetworkIPv6 = makeNorNetIP(providerIndex, siteIndex, NorNet_NodeIndex_Tunnelbox, 6).ip

            # ====== External addresses =====================================
            output.beginRow()
            if showAddresses == True:
               t = 4
            else:
               t = 3
            if isp == 1:
               output.cell(output.makeTiny('1'), -1, t * len(providerList))
               output.cell(output.makeTiny('tunnelbox'), nodePlusSliverCells - 1, t * len(providerList))
            else:
               output.multiRowFill()
               output.multiRowFill(nodePlusSliverCells - 1)

            output.cell(output.makeTiny(output.makeURL(providerURL, provider['provider_short_name']) + ' (' + str(provider['provider_index']) + ')'), 2, t)
            if showAddresses == True:
               output.cell(output.makeTiny(str(externalNetworkIPv4)))
            else:
               output.cell(output.makeSmiley(True))
            if externalNetworkIPv6 != IPv6Address('::'):
               if showAddresses == True:
                  output.cell(output.makeTiny(str(externalNetworkIPv6)))
               else:
                  output.cell(output.makeSmiley(True))
            else:
               output.cell(output.makeSmiley(False))
            output.endRow()

            if showAddresses == True:
               output.beginRow()
               output.multiRowFill()
               output.multiRowFill(nodePlusSliverCells - 1)
               output.multiRowFill(2)
               if showAddresses == True:
                  output.cell(output.makeTiny(str(internalNetworkIPv4)))
                  output.cell(output.makeTiny(str(internalNetworkIPv6)))
               else:
                  output.cell(output.makeSmiley(True))
                  output.cell(output.makeSmiley(True))
               output.endRow()

            output.beginRow()
            if provider['provider_type'] == '':
               t = 'unspecified'
            else:
               t = provider['provider_type']
            output.multiRowFill()
            output.multiRowFill(nodePlusSliverCells - 1)
            output.multiRowFill(2)
            output.cell('Type:')
            output.cell(t)
            output.endRow()

            output.beginRow()
            if provider['provider_downstream'] <= 0:
               d = '?'
            else:
               d = str(provider['provider_downstream'])
            if provider['provider_upstream'] <= 0:
               u = '?'
            else:
               u = str(provider['provider_upstream'])
            if ((d == '?') and (u == '?')):
               ud = 'unspecified'
            else:
               ud = d + ' / ' + u + ' Kbit/s'
            output.multiRowFill()
            output.multiRowFill(nodePlusSliverCells - 1)
            output.multiRowFill(2)
            output.cell('Down/Up: ')
            output.cell(ud)
            output.endRow()


   # ====== Research nodes ==================================================
   for node in nodeList:
      if node['node_site_id'] == site['site_id']:
         #print node
         nodeIndex = node['node_index']
         nodeName  = node['node_name'].split('.')[0]
         nodeUTF8  = node['node_utf8']
         nodeState = node['node_state']

         #nodeHost    = ''
         #nodeDisplay = ''
         #nodeTagsList = node['node_tags']
         #for nodeTag in nodeTagsList:
            #if nodeTag['tagname'] == 'fcdistro':
               #nodeFC = nodeTag['value']
            #if nodeTag['tagname'] == 'pldistro':
               #nodePL = nodeTag['value']


         # ====== Get slivers of node =======================================
         sliversOfNode = []
         if showSlivers == True:
            for sliverNode in sliverNodeList:
               if ((sliverNode['node_site_id'] == site['site_id']) and
                   (sliverNode['node_index'] == nodeIndex)):
                  sliversOfNode.append(sliverNode)


         # ====== Root context ==============================================
         endpointList    = extractIPs(siteIndex, nodeIndex)
         isFirstEndpoint = True
         for endpoint in endpointList:
            output.beginRow()
            if isFirstEndpoint == True:
               isFirstEndpoint = False
               output.cell(output.makeTiny(str(nodeIndex)), -1, (1 + len(sliversOfNode)) * len(providerList))
               output.cell(output.makeTiny(nodeName),       -1, (1 + len(sliversOfNode)) * len(providerList), 'text-align: left;')
               if showSlivers == True:
                  output.cell(output.makeTiny('Root Context'), 2, len(providerList))
            else:
               output.multiRowFill()
               output.multiRowFill()
               if showSlivers == True:
                  output.multiRowFill(2)

            output.cell(output.makeTiny(endpoint[0]), 2)
            output.cell(output.makeTiny(endpoint[1]))
            output.cell(output.makeTiny(endpoint[2]))
            output.endRow()


         # ====== Sliver context ============================================
         for sliverNode in sliversOfNode:
            sliceNodeIndex  = sliverNode['pseudo_slice_node_index']
            sliverShortName = sliverNode['node_name'].split('.')[0]

            endpointList    = extractIPs(siteIndex, nodeIndex, sliceNodeIndex)
            isFirstEndpoint = True
            for endpoint in endpointList:
               output.beginRow()
               output.multiRowFill()
               output.multiRowFill()
               if isFirstEndpoint == True:
                  isFirstEndpoint = False
                  output.cell(output.makeTiny(str(sliverNode['pseudo_slice_node_index'])), -1, len(providerList))
                  output.cell(output.makeTiny(sliverShortName), -1, len(providerList), 'text-align: left;')
               else:
                 output.multiRowFill()
                 output.multiRowFill()

               output.cell(output.makeTiny(endpoint[0]), 2)
               output.cell(output.makeTiny(endpoint[1]))
               output.cell(output.makeTiny(endpoint[2]))
               output.endRow()

   output.endTable()


# ====== Get slice details ==================================================
if showSlivers == True:
   output.header(headerBaseLevel, 'cha:Slice-Details', 'Slice Details')

   output.beginTable(4)

   output.beginRow()
   output.cell('No.')
   output.cell('Slice Name')
   output.cell('Description', 2, -1, 'width: 20em;')
   output.endRow()

   sliceNumber = 0
   for slice in sorted(sliceList, key = lambda x: x['slice_name']):
      #print slice
      sliceNumber = sliceNumber + 1
      output.beginRow()
      output.cell(str(sliceNumber), -1, 3)
      output.cell(output.addToIndex(output.makeText(str(slice['slice_name']))) + output.addToIndex('Slice!' + output.makeText(str(slice['slice_name']))) + output.makeText(str(slice['slice_name'])), -1, 3)
      output.cell(output.makeText(str(slice['slice_description'])), 2, -1, 'text-align: justify; width: 30em;')
      output.endRow()

      output.beginRow()
      output.multiRowFill()
      output.multiRowFill()
      output.cell('URL:', -1, -1, 'text-align: left; width: 5em;')
      output.cell(output.makeURL(slice['slice_url'], slice['slice_url']), -1, -1, 'text-align: left; width: 22.5em;')
      output.endRow()

      output.beginRow()
      output.multiRowFill()
      output.multiRowFill()
      output.cell('Slivers:', -1, -1, 'text-align: left;')
      output.cell(str(len(slice['slice_node_ids'])), -1, -1, 'text-align: left;')
      output.endRow()

   output.endTable()

output.comment('END-OF-SITES-BLOCK')
