'''
Created on Sep 14, 2013

@author: paepcke

Modifications:
  - Jan 1, 2013: added remove() method to OutputFile
  
'''
import StringIO
from collections import OrderedDict
import csv
import re
import sys
import os
import tempfile

from col_data_type import ColDataType


class OutputDisposition(object):
    '''
    Specifications for where completed relation rows
    should be deposited, and in which format. Current
    output options are to files, and to stdout.
    This class is abstract, but make sure the subclasses
    invoke this super's __init__() when they are initialized.
    
    Also defined here are available output formats, of
    which there are two: CSV, and SQL insert statements AND
    CSV.
    NOTE: currently the CSV-only format option is broken. Not
    enough time to maintain it.
    SQL insert statements that are directed to files will also
    generate equivalent .csv files. The insert statement files
    will look like the result of a mysqldump, and inserts into
    different tables are mixed. The corresponding (values-only)
    csv files are split: one file for each table.  
    '''
    def __init__(self, outputFormat, outputDestObj=None):
        '''

        :param outputDestObj: instance of one of the subclasses
        :type outputDestObj: Subclass(OutputDisposition)
        
        '''
        self.outputFormat = outputFormat
        if outputDestObj is None:
            self.outputDest = self
        else:
            self.outputDest = outputDestObj
        self.csvTableFiles = {}
        self.schemas = TableSchemas()
    
    def __enter__(self):
        return self.outputDest
        
    def __exit__(self,excType, excValue, excTraceback):
        try:
            self.outputDest.close()
        except:
            # If the conversion itself went fine, then
            # raise this exception from the closing attempt.
            # But if the conversion failed, then have the 
            # system re-raise that earlier exception:
            if excValue is None:
                raise IOError("Could not close the output of the conversion: %s" % sys.exc_info()[0])

        # Return False to indicate that if the conversion
        # threw an error, the exception should now be re-raised.
        # If the conversion worked fine, then this return value
        # is ignored.
        return False

    def flush(self):
        self.outputDest.flush()

    def getOutputFormat(self):
        return self.outputFormat

    def addSchemaHints(self, tableName, schemaHints):
        '''
        Provide a schema hint dict for the table of the given name.

        :param tableName: name of table to which schema applies. The name may be None, in which case it refers to the main (default) table.
        :type tableName: String
        :param schemaHints: dict mapping column names to SQL types via ColumnSpec instances
        :type schemaHints: [ordered]Dict<String,ColumnSpec>
        '''
        self.schemas.addColSpecs(tableName, schemaHints)

    def getSchemaHint(self, colName, tableName):
        '''
        Given a column name, and a table name, return the ColumnSpec object
        that describes that column. If tableName is None, the main (default)
        table's schema will be searched for a colName entry

        :param colName: name of column whose schema info is sought
        :type colName: String
        :param tableName: name of table in which the given column resides
        :type tableName: String
        :return: list of ColumnSpec instances

        :rtype: (ColumnSpec)
        @raise KeyError: if table or column are not found 
        '''
        return self.schemas[tableName][colName]

    def getSchemaHintByPos(self, pos, tableName):
        try:
            return self.schemas[tableName].values()[pos]
        except ValueError:
            return None
        except IndexError:
            raise ValueError("Attempt to access pos %s in schema for table %s, which is shorter than %s: %s") %\
                (str(pos), tableName, str(pos), self.schemas[tableName].values())

    def getSchema(self, tableName):
        try:
            return self.schemas[tableName].values()
        except ValueError:
            return None

    def copySchemas(self, destDisposition):
        '''
        Given another instance of OutputDisposition, 
        copy this instance's schemas to the destination.
        :param destDisposition: another instance of OutputDisposition
        :type destDisposition: OutputDisposition
        '''
        destDisposition.schemas = self.schemas
    
    def ensureColExistence(self, colName, colDataType, jsonToRelationConverter, tableName=None):
        '''
        Given a column name and MySQL datatype name, check whether this
        column has previously been encountered. If not, a column information
        object is created, which will eventually be used to create the column
        header, or SQL alter statements.

        :param colName: name of the column to consider
        :type colName: String
        :param colDataType: datatype of the column.
        :type colDataType: ColDataType
        :param tableName: name of table to which the column is to belong; None if for main table
        :type tableName: {String | None}
        '''
        schemaDict = self.schemas[tableName]
        if schemaDict is None or len(schemaDict) == 0:
            # schema for this table definitely does not have the column:
            colSpecObj
 = ColumnSpec( colName, colDataType, jsonToRelationConverter)
            self.schemas[tableName] = OrderedDict({colName : colSpecObj})
            return
        # Have schema (dict) for this table. Does that dict contain
        # an entry for the col name?
        try:
            schemaDict[colName]
            # all set:
            return
        except KeyError:
            colSpecObj = ColumnSpec( colName, colDataType, jsonToRelationConverter)
            schemaD
ict[colName] = colSpecObj

    def createTmpTableFile(self, tableName, fileSuffix):
        '''
        Used for cases in which parsers must create more than one
        table. Those tables need to be written to disk, even when
        output of the main table is piped.

        :param tableName: name by which the table file obj can be retrieved 
        :type tableName: String
        :param fileSuffix: suffix for temp file name. Ex. 'csv' for CSV outputs, or 'sql' for SQL dumps
        :type fileSuffix: String
        :return: file object open for writing

        :rtype: File
        '''
        self.csvTableFiles[tableName] = tempfile.NamedTemporaryFile(prefix='tmpTable', 
                                                                    suffix=fileSuffix)
        return self.csvTableFiles[tableName]
        
        
    #--------------------- Available Output Formats
      
    class OutputFormat():
        CSV = 0
        SQL_INSERT_STATEMENTS = 1
        SQL_INSERTS_AND_CSV = 2
            
#--------------------- Available Output Destination Options:  
        
class OutputPipe(OutputDisposition):
    
    def __init__(self, outputFormat):
        super(OutputPipe, self).__init__(outputFormat)
        self.fileHandle = sys.stdout
        # Make file name accessible as property just like 
        # Python file objects do:
        self.name = "<stdout>"  # @UnusedVariable
        self.csvWriter = csv.writer(sys.stdout, dialect='excel', delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
        self.tableCSVWriters = {}
        
    def close(self):
        pass # don't close stdout
    
    def flush(self):
        sys.stdout.flush()
    
    def __str__(self):
        return "<OutputPipe:<stdout>"

    def writerow(self, colElementArray, tableName=None):
        # For CSV: make sure everything is a string:
        if self.outputFormat == OutputDisposition.OutputFormat.CSV:
            row = map(str,colElementArray)
            if tableName is None:
                self.csvWriter.writerow(row)
            else:
                self.tableCSVWriters[tableName].writerow(row)
        else:
            print(colElementArray)
            

    def startNewTable(self, tableName, 