#!/usr/bin/env python

import os
blib = os.environ.get("BIRCHPYLIB")
import sys
sys.path.append(blib)

#import birchenv
#import birchscript
import os.path
import re
import shutil

'''
blsort.py - Sort a table from a csv or tsv file by columns
Synopsis: blsort.py infile outfile  [-cols comma-separated-list] [-descending] [-sep seperator]

  infile -  input file
  outfile - output file
  -cols <integer>[,<integer>] 
  -descending
  -sep - separator character to use when input is a csv file eg tab, comma
  
@modified: January 22, 2019
@author: Brian Fristensky
@contact: brian.fristensky@umanitoba.ca  
'''

from birchlib import Birchmod
from birchlib import Argument

PROGRAM = "blsort.py : "
USAGE = "\n    USAGE: blsort.py infile outfile [-cols <integer>[,<integer>]] [-descending] [-sep <seperator>]"

BM = Birchmod(PROGRAM, USAGE)

DEBUG = True

#--------------------------- Parameters -----------------------------
class Parameters:
    """
          Wrapper class for command line parameters
          """
    def __init__(self):
        """
           Initializes arguments:
             IFN=""
             COLS=[1]
                DESCENDING = False
                SEPERATOR = "TAB"
             OFN=""

           Then calls read_args() to fill in their values from command line
          """
        self.IFN = ""
        self.COLS = [1]
        self.DESCENDING = False
        self.SEPERATOR = "\t"
        self.OFN = ""
        self.read_args()

    def read_args(self):
        """
        Read command line arguments into a Parameter object

        """

        infile = Argument("", str, BM)
        outfile = Argument("", str, BM)    
        cols = Argument("-cols", str, BM)
        descending = Argument("-descending", str, BM)
        seperator = Argument("-sep", str, BM)


        infile.set_position(1)
        outfile.set_position(2)
        cols.set_optional()
        descending.set_optional()
        descending.set_is_switch()
        seperator.set_optional()
        
        try:
            self.IFN = infile.fetch()
            self.OFN = outfile.fetch()
            if BM.arg_given('-cols') :
                colstring = cols.fetch()
                tempcols = colstring.split(',')
                # cast list of columns into int list
                self.COLS = []
                for field in tempcols : 
                    self.COLS.append(int(field))  
            if BM.arg_given('-descending') :            
                self.DESCENDING = descending.fetch()
            if  BM.arg_given('-sep') :
                self.SEPERATOR = seperator.fetch()

        except:
            BM.printusage()

        if DEBUG :
            print('INFILE: ' + self.IFN)
            print('OUTFILE: ' + self.OFN)
            print('COLS: ' + str(self.COLS))
            print('DESCENDING: ' + str(self.DESCENDING))
            print('SEPERATOR: ' + self.SEPERATOR)


#--------------------------- Table -----------------------------
class Table:
    """
          Implements a table as a list of lists. self.T is a list of rows.
          Each row is a list of fields (columns).
          """
    def __init__(self):
        """
        Has methods for reading, writing, and sorting a table
          """
        self.Header = []
        self.TUnsorted = [] # Original unsorted rows
        self.TCantSort = [] # Rows with empty fields in sort columns
        self.TSorted = [] # Rows after sorting
        self.TWidth = 0
        self.numrows = 0

    # !!! We need to come up with a way to convert a column that contains numbers, represented
    # by strings, into integers... and back again when we print.

    # - - - - - - - - - - - - - - - - -
    def read_table(self,IFN,COLS,SEP):
        """
        Read a table as a list of lines

        Convert each column to a type that can be sorted
        as the user might reasonably expect. 

        currency - not yet implemented
        date - not yet implemented  
        
        Bug - does a very crude check for the width of the table in
        columns. This will only work if all rows have exactly the same
        number of columns. Need to modify code to pad with empty columns
        """

        # If a row has missing fields, or null fields, it is unsortable.
        def Sortable(temp) :
            Okay = True
            for c in COLS :
                if c > len(temp) :
                    Okay = False
            return Okay       

        def RemoveUnsortable():
            while len(self.TUnsorted) > 0 :
                temp = self.TUnsorted.pop(0)
                if Sortable(temp) :
                    self.TSorted.append(temp)
                else :
                    self.TCantSort.append(temp)                    

        def ParseColumns() :

           # return true if column is float
           def testfloat(value) :
               result = False
               try :
                   F = float(value)
                   result = True
               except ValueError, ve :
                   pass
               return result

           # return true if column is int
           def testint(value) :
               result = False
               try :
                   F = int(value)
                   result = True
               except ValueError, ve :
                   pass
               return result

           # - - - - - - - - ParseColumns main - - - - - - - - -
           # For each column x, cast values into one of float, integer, or string 
           for x in range(0, self.TWidth) :

               # First, let's see ALL items in the column are integer
               # Test all rows in the column until something cannot be parsed as an integer
               ColumnType="integer"
               y = 0
               while (y < self.numrows) and (ColumnType=="integer") :
                   if not testint(self.TUnsorted[y][x]) :
                       ColumnType = "other"
                   y += 1

               if ColumnType == "other" :
                   # See if ALL items in the column are float
                   ColumnType="float"
                   y = 0
                   while (y < self.numrows) and (ColumnType=="float") :
                       if not testfloat(self.TUnsorted[y][x]) :
                           ColumnType = "other"
                       y += 1

               if ColumnType == "other" :
                   ColumnType = "string"  
  
               if DEBUG :
                   print 'Column ' + str(x) + ': ' + ColumnType

               #if not a string, convert all cells in the column into the appropriate type
               if ColumnType == "float" :
                   y = 0
                   for row in self.TUnsorted :
                       self.TUnsorted[y][x] = float(self.TUnsorted[y][x])
                       y += 1
               if ColumnType == "integer" :
                   y = 0
                   for row in self.TUnsorted :
                       self.TUnsorted[y][x] = int(self.TUnsorted[y][x])
                       y += 1

        # . . . . . . . . read_table main . . . . . . . 
        h_infile = open(IFN,'r')
        self.TWidth = 0
        for Line in h_infile:
            row = Line.split(SEP)
            # Remove double quotes that enclose fields, if any
            # Also remove leading and trailing whitespace
            row = [field.strip().translate(None,'"') for field in row]

            # If a comment, add the original line to the header list.
            # Otherwise, add the parsed row to the table.
            # Empty lines in the input are ignored.
            if len(row) > 0 :
                if row[0].startswith('#') :
                    self.Header.append(Line.strip())
                else:
                    self.TUnsorted.append(row)   
                    if len(row) > self.TWidth :
                        self.TWidth = len(row)  
        h_infile.close()
        self.numrows = len(self.TUnsorted)

        RemoveUnsortable()
        #ParseColumns()


    # - - - - - - - - - - - - - - - - - - - - - -
    def sort_table(self,COLS,DESCENDING):
        """
        Sort a table using COLS as keys, in ascending or descending order

        """ 
        from operator import itemgetter  
     
        # colitems = []
        #for item in COLS :
            # list indices begin with 0, so we have to subtract 1
            #colitems.append(int(item)-1)

        #self.TSorted = sorted(self.TSorted, key=itemgetter(*tuple(COLS)), reverse=DESCENDING)


    # Write the rows of a table to a file
    def write_rows(F,TBL,SEP) :
        for row in TBL :
            n = 0
            LENGTH = len(row)
            if n < LENGTH :
                F.write(str(row[n]))
                n = n + 1
            while n < LENGTH :
                F.write(SEP + str(row[n]))
                n = n + 1
            F.write('\n')   

    # - - - - - - - - - - - - - - - - - -
    def write_table(self,OFN,SEP,DESCENDING):
        """
        Write a table 

        """

        h_outfile = open(OFN,'w')

        # write out header lines
        for line in self.Header :
            h_outfile.write(line + '\n')

        # write out table lines
        if DESCENDING :
            write_rows(h_outfile,self.TSorted,SEP)
            write_rows(h_outfile,self.TCantSort,SEP)
        else :
            write_rows(h_outfile,self.TCantSort,SEP)
            write_rows(h_outfile,self.TSorted,SEP)
                    
        h_outfile.close()

#======================== MAIN PROCEDURE ==========================
def main():
    """
        Called when not in documentation mode.
        """
    # Read parameters from input file
    P = Parameters()    

    # Read table from infile
    TBL = Table()
    TBL.read_table(P.IFN,P.COLS,P.SEPERATOR) 

    # Sort table
    TBL.sort_table(P.COLS,P.DESCENDING)
    
    # Write sorted table to output file
    TBL.write_table(P.OFN,P.SEPERATOR,P.DESCENDING)  
       
    BM.exit_success()

if (BM.documentor() or "-test" in sys.argv):
    pass
else:
    main()





