#!/usr/bin/env python
# -*- coding:utf-8 -*-
# 日本語です。
# Converts an Excel file with directions for guideboards into a LaTeX file.
# Usage:
#   bin/makeSigns.py signs.xlsx command [style] >signs.inc
#
# Excel file (signs.xlsx) format
#   comment:    # to comment out
#   ID:         id for the guideboard
#   location:   location of the guideboard
#   command:    LaTeX command
#               If the command ends with '*', and multiple board directions are
#               given, backward direction is omitted for some commands.
#   style:      style of panel
#   boardDirection: directions that the boards faces
#               (N, NE, E, SE, S, SW, W, NW, separated by commas)
#   maps:       filenames of maps used for each direction, seprated by commaas
#               none: no map is shown
#               If one file is given, the file is used for all the directions.
#               Otherwise, the same number of maps as the number of directions
#               should be given.
#   text at fixed location #1
#   text at fixed location #2
#   text at the top
#   text between the map and table
#   text at the bottom
#   List of following pairs
#   text0:       LaTeX string (such as building names)
#   direction0:  direction for the buildings
#               Only one from N, NE, E, SE, S, SW, W, NW.
#               For "toEntrance" command, direction preceeded with "*" 
#               such as "*E" indicates the direction of the entrance.
#   text1, direction1, text2, direction2, ...
#
# 2016-10-09 Taku Yamanaka (Osaka Univ.)
# v1.0 2016-10-09
# v1.1 2016-10-09
#       Added NE, SE, SW, and NW in directions.
#       Added ForwardRight, ForwardLeft, ...
# v2.0  2016-10-13 Taku
#       Support placing texts at fixed locations using picture env.
#       Added texts at top, middle, and bottom.
# v2.1  2016-10-18 Taku
#       Added "command" column in Excel file.
# v2.1.1 2016-10-22 Taku
#       Added print2ColArrows and printGroupedArrows.
# v2.2  2016-10-24 Taku
#       Added "Group" option to group texts.
# v2.3  2016-11-03 Taku
#       Made DirectionList, TextDirec, and Direction classes.
# v2.4  2016-11-14 Taku
#       Added largeArrow.
#       2016-12-10
#       toEntrance now handles 8 directions for the entrance.
#       Added printEntrance.
# v2.5  2017-03-26 Taku
#       Reads all the sheets in the input Excel file.
#       Omits backward direction only if '*' is added 
#       at the end of LaTeX command.

import sys
import codecs
import xlrd

class Direction(unicode):
    nCompassDirs = 8
    compassNo = {'N':0, 'NE':1, 'E':2, 'SE':3, 
                 'S':4, 'SW':5, 'W':6, 'NW':7,
                 'F':8, 'FR':9, 'R':10, 'BR':11,
                 'B':12, 'BL':13, 'L':14, 'FL':15}

    def __init__(self, *args, **kwargs):
        s = self
        if s[0] == '*':
            s = s[1:]

        if s not in self.compassNo:
            raise ValueError \
                    ('Unknown direction: ' + self + '\n' + \
                '      Please use one of N, NE, E, SE, S, SW, W, and NW,\n' + \
                '      or one of F, FR, R, BR, B, BL, L, and FL.'
                    )

    def dirNo(self):
        s = self
        if s[0] == '*':
            s = s[1:]
        return self.compassNo[s]

    def relativeDirNo(self, dirStr):
        if self.dirNo() < self.nCompassDirs: # absolute direction
            return ( self.dirNo() - dirStr.dirNo() ) % self.nCompassDirs

        else:  # relative direction
            return self.dirNo() - self.nCompassDirs

class TextDirec:
    def __init__(self, text, direc):
        self.text = text

        if type(direc) == unicode:
            self.direc = Direction(direc)
        else:
            self.direc = direc  # int: relative direction

class DirectionList(list):
    def dump(self):
        for txtDir in self:
            print >>sys.stderr, txtDir.text + ':', txtDir.direc

    def relativeDirs(self, boardDirec):
        relDirs = []
        for txtDir in self:
            relDirNo = txtDir.direc.relativeDirNo(boardDirec)
            relDirs.append(TextDirec(txtDir.text, relDirNo))

        return DirectionList(relDirs)

    def selectDirs(self, selectionFilter):
        filteredList = []
        for txtDir in self:
            if txtDir.direc in selectionFilter:
                filteredList.append(txtDir)

        return DirectionList(filteredList)

    def removeDirs(self, removalFilter):
        filteredList = []
        for txtDir in self:
            if txtDir.direc not in removalFilter:
                filteredList.append(txtDir)

        return DirectionList(filteredList)

    def groupDirections(self, groupOrder, textSeparator):
        textDirecList = [ [] for i in range(Direction.nCompassDirs)]
        for txtDir in self:
            textDirecList[txtDir.direc].append(txtDir.text)

        groupedTextDirs = []
        for group in groupOrder:
            textList = textDirecList[group]
            if len(textList) > 0:
                groupedTextDirs.append( \
                        TextDirec(textSeparator.join(textList), group))

        return DirectionList(groupedTextDirs)

class Panel:
    commandStr = ('\\Forward',  '\\ForwardRight', 
                  '\\Right',    '\\BackRight', 
                  '\\Back',     '\\BackLeft', 
                  '\\Left',     '\\ForwardLeft')

    kF, kFR, kR, kBR, kB, kBL, kL, kFL = range(8)
    frontRightLeftBack = [kF, kFR, kR, kBR, kFL, kL, kBL, kB]
    frontLeftRightBack = [kF, kFL, kL, kBL, kFR, kR, kBR, kB]
    leftFrontBackRight = [kL, kFL, kBL, kF, kB, kBR, kFR, kR]
    defaultOrder = frontRightLeftBack

    # leftSide = [kFL, kL, kBL, kB]
    # rightSide = [kF, kFR, kR, kBR]
    leftSide = [kF, kFL, kL, kBL]
    rightSide = [kFR, kR, kBR, kB]

    groupedDirOrder = {
            'mapArrows': defaultOrder,
            'twoColArrows': defaultOrder,
            'toEntrance':   defaultOrder,
            'largeArrow':    defaultOrder,
            'singleArrow':    defaultOrder,
            'arrowBanner':  leftFrontBackRight }

    groupedTextSeparator = {
            'mapArrows': '\\\\',
            'twoColArrows': '\\\\',
            'toEntrance':   '\\\\',
            'largeArrow':    ' ',
            'singleArrow':    ' ',
            'arrowBanner':  ' '}

    groupedDirectionsSeparator = {
            'mapArrows': '\hline',
            'twoColArrows': '\hline',
            'toEntrance':   '\hline',
            'largeArrow':    '',
            'singleArrow':    '',
            'arrowBanner':  ''}

    # Directions to remove if multiple boards are assigned for one location.
    removeDirs = {
            'mapArrows': [kB],
            'twoColArrows': [kB],
            'toEntrance':   [kB],
            'largeArrow':    [],
            'singleArrow':    [],
            'arrowBanner':  [kB] }

    def __init__(self, ID, location, command, groupOpt, \
            boardDirections, maps, texts, textDirs):
        self.ID = ID
        self.location = location
        self.command = command.replace('*', '')
        self.removeBack = command[-1] == '*'
        self.groupDestinations = (groupOpt == 'G') or (groupOpt == 'g')
        self.tightGroup = (groupOpt == 'G')
        if command == 'arrowBanner' or command == 'largeArrow' or \
           command == 'singleArrow':
            self.groupDestinations = True

        tmpBoardDirs = boardDirections.split(',')
        self.boardDirections = []
        for bd in tmpBoardDirs:
            try:
                self.boardDirections.append(Direction(bd.strip()))
            except ValueError, e:
                print >>sys.stderr, 'Error at ID=', self.ID, ':', e

        self.maps = maps.split(',')
        self.texts = texts
        try:
            self.directionList = DirectionList(textDirs)
        except ValueError, e:
            print >>sys.stderr, 'Error at ID=', self.ID, ':', e


        if len(self.maps) > 1 and len(self.maps) != len(self.boardDirections):
            print >>sys.stderr, 'Error: ID=', self.ID,
            print >>sys.stderr, 'The number of maps (', len(self.maps), 
            print >>sys.stderr, ') should match the number of boardDirections (',
            print >>sys.stderr, len(self.boardDirections), ').'

    def dump(self):
        print >>sys.stderr, self.ID, self.location, \
                self.command, self.groupDestinations
        print >>sys.stderr, self.boardDirections, self.maps
        self.directionList.dump()

    def makeTeXStringForDir(self, relativeDirectionList):
        if len(self.boardDirections) > 1 and self.removeBack:
            removeList = self.removeDirs[self.command]
            relativeDirectionList = relativeDirectionList.removeDirs(removeList)

        if self.groupDestinations:
            textSeparator = self.groupedTextSeparator[self.command]
            if self.tightGroup:
                textSeparator = ' '

            relativeDirectionList \
                = relativeDirectionList.groupDirections(\
                                    self.groupedDirOrder[self.command], \
                                    textSeparator)
            dirSep = self.groupedDirectionsSeparator[self.command]

        texStringList = []
        for txtDir in relativeDirectionList:
            texStr = '\t\t' + self.commandStr[txtDir.direc] \
                    + '{' + txtDir.text + '}'

            if self.groupDestinations and dirSep != '' and \
                    len(texStringList) > 0:
                texStringList.append(dirSep)

            texStringList.append(texStr)

        return texStringList

    def printMapArrows(self):
        jmap = 0
        for boardDirec in self.boardDirections:
            print '\\mapArrows{' + self.ID + '-' + boardDirec + '}{' \
                    + self.location + '}'

            mapFile = self.maps[jmap].strip()
            print '\t{' + mapFile + '}'

            for txt in texts:
                print '\t{' + txt + '}'

            relativeDirectionList = self.directionList.relativeDirs(boardDirec)

            texStringList = self.makeTeXStringForDir(relativeDirectionList)

            print '\t{%'
            print '\n'.join(texStringList)
            print '\t}%'
            
            if len(self.maps) > 1:
                jmap += 1

    def print2ColArrows(self):
        for boardDirec in self.boardDirections:
            print '\\twoColArrows{' + self.ID + '-' + boardDirec + '}{' + self.location + '}'

            relativeDirectionList = self.directionList.relativeDirs(boardDirec)
            leftCol  = relativeDirectionList.selectDirs(self.leftSide)
            rightCol = relativeDirectionList.selectDirs(self.rightSide)

            for txt in texts:
                print '\t{' + txt + '}'
        
            for colTextDirs in (leftCol, rightCol):
                texStringList = self.makeTeXStringForDir(colTextDirs)

                print '\t{%'
                print '\n'.join(texStringList)
                print '\t}%'

    def findEntrance(self):
        entranceDirec = []
        otherDirec = []

        for txtDir in self.directionList:
            if txtDir.direc[0] == '*':
                entranceDirec.append(txtDir)

            else:
                otherDirec.append(txtDir)

        if len(entranceDirec) != 1:
            raise ValueError\
                ('One and only one cell should have * to show the entrance.')

        return entranceDirec[0], DirectionList(otherDirec)

    def printToEntrance(self):
        entranceDir, otherDirList = self.findEntrance()

        for boardDirec in self.boardDirections:
            print '\\toEntrance{' + self.ID + '-' + boardDirec + '}{' + self.location + '}'
            relDirNo = entranceDir.direc.relativeDirNo(boardDirec)
            if 1 <= relDirNo <= 4:
                blueSide = 'Right'
            else:
                blueSide = 'Left'

            # if relDirNo != 2 and relDirNo != 6:
            #     raise ValueError\
            #         ('Entrance should be either on left or right for ID=' + self.ID)

            text = entranceDir.text

            fontsize = ''
            if len(text) > 1:
                fontsize = '\\entnormal '

            # print '\t{' + self.commandStr[relDirNo][1:] + '}{' + fontsize + text + '}'
            print '\t{' + blueSide + '}{\\Ent' + self.commandStr[relDirNo][1:] + '{' + fontsize + text + '}}'

            for txt in texts[2:]:
                print '\t{' + txt + '}'

            relativeDirectionList = otherDirList.relativeDirs(boardDirec)
            texStringList = self.makeTeXStringForDir(relativeDirectionList)

            print '\t{%'
            print '\n'.join(texStringList)
            print '\t}%'

    def printLargeArrow(self):
        for boardDirec in self.boardDirections:
            relativeDirectionList = self.directionList.relativeDirs(boardDirec)
            texStringList = self.makeTeXStringForDir(relativeDirectionList)

            for texStr in texStringList:
                print '\\' + self.command \
                        + '{' + self.ID + '-' + boardDirec + '}{' \
                        + self.location + '}'
                for txt in texts[:2]:
                    print '\t{' + txt + '}'

                print '\t{' + texStr.strip() + '}%'

    def printEntrance(self):
        print '\\' + self.command \
                + '{' + self.ID + '-' + self.boardDirections[0] + '}{' \
                + self.location + '}'
        for txt in texts[:2]:
            print '\t{' + txt + '}'

        print '\t{}%'

    def makeTabularFormatForBanner(self, texStringList):
        formatList = []

        for texStr in texStringList:
            arrowFirst = False

            if 'Left{' in texStr:
                formatList.append('cX')

            else:
                formatList.append('Xc')

            # if 'Left' in texStr:
            #     arrowFirst = True

            # if 'Forward' in texStr:
            #     arrowFirst = True

            # if arrowFirst:
            #     formatList.append('cL')
            # else:
            #     formatList.append('RR')

        blankRow = '&'*(len(texStringList)*2 - 1) + '\\\\'

        return '|'.join(formatList), blankRow

    def printArrowBanner(self):
        for boardDirec in self.boardDirections:
            relativeDirectionList = self.directionList.relativeDirs(boardDirec)
            texStringList = self.makeTeXStringForDir(relativeDirectionList)

            tabularFormat, blankRow \
                    = self.makeTabularFormatForBanner(texStringList)

            print '\\arrowBanner{' + self.ID + '-' +  boardDirec + '}{' + self.location + '}'

            for txt in texts[:2]:   # texts at fixed locations only
                print '\t{' + txt + '}'
        
            print '\t{' + tabularFormat + '}'
            print '\t{' + blankRow + '}'
            print '\t{%'
            print ' & \n'.join(texStringList)
            print '\t\t\\\\'
            print '\t}%'


    def printPanels(self):
        if self.command == 'mapArrows':
            self.printMapArrows()

        elif self.command == 'toEntrance':
            self.printToEntrance()

        elif self.command == 'twoColArrows':
            self.print2ColArrows()

        elif self.command == 'largeArrow':
            self.printLargeArrow()

        elif self.command == 'singleArrow':
            if len(self.directionList) == 0:
                self.printEntrance()
            else:
                self.printLargeArrow()

        elif self.command == 'arrowBanner':
            self.printArrowBanner()

        else:
            print >>sys.stderr, 'ERROR-Unknown command', self.command

def readExcelRow(sheet, row):
    items = []
    for col in range(sheet.ncols):
        cell = sheet.cell(row, col).value
        items.append(cell)

    rawid, location, command, style, groupOpt, boardDirections, maps = items[1:jtext0]
    ID = str(rawid)

    texts = items[jtext0:jtext1]

    textDirs = []
    for j in range(jtext1, sheet.ncols, 2):
        direc, text = items[j:j+2]
        if text != '' and direc != '':
            # textDirs.append((text, direc))
            textDirs.append(TextDirec(text, direc))

    return ID, location, command, style, groupOpt, boardDirections, maps, texts, textDirs

#--------- main -----------
sys.stdout = codecs.getwriter('utf_8')(sys.stdout)
sys.stderr = codecs.getwriter('utf_8')(sys.stderr)

excelFile = sys.argv[1]
selectedCommand = sys.argv[2]   # command to select
selectedStyle = ''
if len(sys.argv) > 3:
    selectedStyle = sys.argv[3] # panel size to select

book = xlrd.open_workbook(excelFile)

jcommand = 3 # LaTeX command
jstyle = 4 # panel size
jtext0 = 8  # cell number (0 origin) of text for the first fixed location
jtext1 = 13 # cell number of text0 for directions

for sheet in book.sheets():
# sheet = book.sheet_by_index(0)
    for row in range(sheet.nrows):
        if sheet.cell(row, 0).value == '' and \
           sheet.cell(row, jcommand).value.replace('*', '') == selectedCommand and \
           sheet.cell(row, jstyle).value == selectedStyle:
            ID, location, command, style, groupOpt, boardDirections, maps, texts, textDirs \
                    = readExcelRow(sheet, row)
            panel = Panel(ID, location, command, groupOpt, boardDirections, maps, texts, textDirs)
            # panel.dump()
            panel.printPanels()
