
"""
    Working with Excel can be tedious.

    It's extremely easy to build terrible worksheets and there is no enforcement
    in data integrity -- anyone can just slap in some data where ever they feel like.
    Usually, this results in workbooks that are like a dirty laundry basket,
    where dats is tossed together and draped over the edges

    https://imgs.xkcd.com/comics/algorithms.png

    In VBA, uou'll spend A LOT of time constantly finding range boundaries in the sheets,
    determining last row or last column before you can start working on the data.
    It's also a big vulnerability to use arbitrary alpha column (A-Z)
    references which lead to invalid references in the VBA code whenever
    columns are shifted or reordered. Ideally, it would be nice to
    attach header names to the rows you are iterating over, much more like
    fields in a database table

    Excel's flexibility is its greatest weakness, but also its greatest strength.
    It's the best software in the world for fast prototyping and reviewing data,
    it just shouldn't be used to build entire processes. For middle-sized data,
    Excel can be phenominally powerful.

    Published site-packages like xlrd, xlwt, openpyxl will allow you to
    read Excel files from python, but don't control the actual application,
    limiting your ability to manage any existing infrastructure built on Excel

    The excel_levity_cls is meant to make Excel data feel as light as a feather
    instead of like busting concrete
"""


from time import sleep

import vengeance as veng
from examples import excel_project_template as share

xlPasteColumnWidths = 8

xlCalculationManual    = -4135
xlCalculationAutomatic = -4105

xlNone   = -4142
xlYellow = 65535
xlBlue   = 15773696
xlPink   = 9856255


@veng.print_runtime
def main():
    share.open_project_workbook(open_new_instance=True,
                                read_only=True,
                                update_links=True)

    instantiate_lev('sheet1')
    # instantiate_lev('sheet2')
    # instantiate_lev('empty sheet')
    # instantiate_lev('jagged rows')

    # lev_subsections()

    iterate_primitive_rows()
    iterate_flux_rows()
    iterate_excel_errors()

    # write_values()
    # write_values_from_lev()
    # append_values()
    # write_formulas()

    # modify_range_values(iter_method='slow')
    modify_range_values(iter_method='fast')

    # excel_object_model()
    # allow_focus()

    # share.close_project_workbook(save=False)


def instantiate_lev(tab_name):
    """
    excel_levity_cls range reference syntax:
        returns win32com reference to Excel range

        lev['<col><row>:<col><row>']
        anchor reference mnemonics:
            '*h': header
            '*f': first
            '*l': last
    """
    # help(veng.excel_levity_cls)

    lev = share.tab_to_lev(tab_name)
    a = repr(lev)
    a = lev.is_empty

    a = lev.header_r
    a = lev.first_c
    a = lev.last_c
    a = lev.first_r
    a = lev.last_r
    a = lev.append_r

    a = lev.has_headers
    a = lev.headers
    a = lev.header_values

    a = lev.named_ranges
    a = lev['some_named_range_1'].Value
    a = lev['some_named_range_2'].Value
    a = lev['excel_date'].Value

    # examples of excel_levity_cls reference syntax
    if 'col_b' in lev.headers:
        a = lev['col_b first_r : col_b last_r'].Address

    a = lev['first_c header_r : last_c last_r'].Address
    a = lev['*f *h:*l *l'].Address
    a = lev['A*h:E*l'].Address

    # or lev.sheet.Range('...') for literal addresses
    a = lev['A1'].Value
    a = lev['A1:C10'].Value

    lev['*f *h:*l *l'].Interior.Color = xlPink
    lev['*f *f:*l *l'].Interior.Color = xlBlue

    lev.clear_filter()
    lev.remove_filter()
    lev.reapply_filter()


def lev_subsections():
    """
    row 1 (colored grey) in the Excel worksheets can be used as subsection markup,
    although any other column reference will work

    eg:
        share.lev_subsection('subsections', '<sect_2>', '</sect_2>')
        share.lev_subsection('subsections', 'col_d', 'col_h')
        share.lev_subsection('subsections', 'F', 'H')
    """
    lev_1 = share.lev_subsection('subsections', '<sect_1>', '</sect_1>')
    lev_2 = share.lev_subsection('subsections', '<sect_2>', '</sect_2>')
    lev_3 = share.lev_subsection('subsections', '<sect_3/>', '<sect_3/>')

    a = lev_1.last_r
    b = lev_2.last_r
    c = lev_3.last_r

    lev_1['*f *h:*l *l'].Interior.Color = xlYellow
    lev_2['*f *h:*l *l'].Interior.Color = xlBlue
    lev_3['*f *h:*l *l'].Interior.Color = xlPink

    lev_1.clear('*f *f:*l *l', clear_colors=True)
    lev_2.clear('*f *f:*l *l', clear_colors=True)
    lev_3.clear('*f *f:*l *l', clear_colors=True)

    lev = share.tab_to_lev('subsections')
    lev['*f *h:*l *l'].Interior.Color = xlNone
    lev.reapply_filter()


def iterate_primitive_rows():
    """ iterate rows as a list of primitive values

    .rows(r_1='*h', r_2='*l'):
        * r_1, r_2 are the start and end rows of iteration
          the default values are the specialized anchor references
          starting at header row, ending at last row
        * r_1, r_2 can also be integers corresponding to the rows in the Excel range

    m = list(lev.rows())
        * as full matrix, includes header row

    m = list(lev.rows('*f'))
        * as full matrix, excludes header row

    m = lev['*f *f:*l *l'].Value2
        * returns values directly from Excel range
        * however, there may be reasons you don't want to do this,
          potentially because the matrix returns as read-only tuples,
          and because Excel's error values are not recognized (see iterate_excel_errors())
    """
    # lev = share.tab_to_lev('Sheet1')
    lev = share.tab_to_lev('errors')

    # see docstring for caveats to this
    # m = lev['*f *f:*l *l'].Value2

    for row in lev.rows():
        a = row[0]

    m = list(lev.rows('*f', 10))

    # build new matrix from filtered rows
    m = [lev.header_values]
    for r, row in enumerate(lev.rows('*f')):
        if r % 2 == 0:
            m.append(row)


def iterate_flux_rows():
    """ iterate rows as flux_row_cls objects

    .flux_rows(r_1='*h', r_2='*l'):
        * r_1, r_2 are the start and end rows of iteration
          the default values are the specialized anchor references
          starting at header row, ending at last row
        * r_1, r_2 can also be integers corresponding to the rows in the Excel range

    for row in lev:
        * preferred iteration syntax
        * begins at first row, not header row

    m = list(lev)
        * as full matrix, excludes header row

    m = list(lev.flux_rows())
        * as full matrix, includes header row
    """
    lev = share.tab_to_lev('Sheet1')

    for row in lev:
        a = row.address
        a = row.names
        a = row.values

        a = row.view_as_array       # meant as a debugging tool in PyCharm

        if 'col_a' in lev.headers:
            a = row.col_a
            a = row['col_a']
            a = row[0]

    m = list(lev.flux_rows(5, 10))

    # extract primitive values
    m = [row.values for row in lev]

    # build new matrix from filtered rows
    m = [lev.header_values]
    for r, row in enumerate(lev):
        if r % 2 == 0:
            m.append(row.values)


def iterate_excel_errors():
    lev = share.tab_to_lev('errors')

    # these will return excel's integer error code, be careful
    for row in lev['B3:D6'].Value:
        a = row[-1]

    # reading values with .rows() or .flux_rows() escapes these errors
    for row in lev.rows(3, 3):
        a = row[-1]

    for row in lev.flux_rows(3, 3):
        a = row.col_c


def write_values():
    lev = share.tab_to_lev('Sheet2')
    lev.clear('*f *f:*l *l')

    # write single value
    lev['*f *f'] = 'hello'

    # write single row
    lev['*f *f'] = ['hello', 'hello', 'hello']

    # write single column
    lev['*f *f'] = [['hello'], ['hello'], ['hello'], ['hello'], ['hello'], ['hello']]

    # write rows and columns
    m = [['col_a', 'col_b', 'col_c']]
    m.extend([['blah', 'blah', 'blah']] * 10)

    lev.clear('*f *f:*l *l')
    lev['*f *h'] = m

    # helper function shortcut
    share.write_to_tab('Sheet2', m)

    # Excel dates
    a = lev['excel_date'].Value
    a = lev['excel_date'].Value2

    # convert Excel eopoch float value into datetime
    b = veng.to_datetime(a)

    # Excel only accepts datetime.datetime, not datetime.date
    try:
        lev['excel_date'].Value = b.date()
    except TypeError as e:
        print(e)

    # the excel_levity_cls.__setitem__ protects against this issue
    lev['excel_date'] = b.date()


def write_values_from_lev():
    lev_1 = share.tab_to_lev('Sheet1')
    lev_2 = share.tab_to_lev('Sheet2')

    lev_2.clear('*f *f:*l *l')
    lev_2['*f 5'] = lev_1.rows(5, 10)

    lev_2.clear('*f *f:*l *l')
    lev_2['*f *h'] = lev_1

    # helper function
    share.write_to_tab(lev_2, lev_1)


def append_values():
    lev = share.tab_to_lev('Sheet2')

    lev.clear('*f *f:*l *l')
    a = lev.append_r

    # if sheet is empty: append_r = first_r
    m = [['a', 'a', 'a']] * 10
    lev['*f *a'] = m

    # if data is already present: append_r = last_r + 1
    a = lev.append_r

    m = [['a', 'a', 'a']] * 10
    lev['*f *a'] = m

    # or use helper function
    share.write_to_tab(lev, ['d', 'd', 'd'], r_1='*a')


def write_formulas():
    lev = share.tab_to_lev('Sheet2')
    lev.clear('*f *f:*l *l')

    lev.application.Calculation = xlCalculationManual

    # lev['col_a *f'] = '=(1 + 0)'
    # lev['col_b *f'] = '=(1 + 1)'
    # lev['col_c *f'] = '=(1 + 2)'

    # lev['col_a 4'] = '=({}{} + 10)'.format(lev.headers['col_a'], lev.first_r)
    # lev['col_b 4'] = '=({}{} + 20)'.format(lev.headers['col_b'], lev.first_r)
    # lev['col_c 4'] = '=({}{} + 30)'.format(lev.headers['col_c'], lev.first_r)

    lev['*f *h'] = ['col_a', 'col_b', 'col_c', 'col_d', 'col_e', 'col_f', 'col_h', 'col_i', 'col_j']

    lev['col_a *f'] = '=(1 + 0)'
    lev['col_a *f:*l *f'].FillRight()

    lev['col_a 4'] = '=({}{} + 10)'.format(lev.headers['col_a'], lev.first_r)
    lev['col_a 4:*l 4'].FillRight()

    lev.last_r = 20
    lev['col_a 4:*l *l'].FillDown()
    lev.calculate()


@veng.print_runtime
def modify_range_values(iter_method='slow'):
    """
    although calls like "ws.Range('...').Value" work fine in VBA,
    this is painfully slow for win32com remote procedure calls

    value extraction into flux_cls can also be used for more complex transformations
    """
    lev = share.tab_to_lev('Sheet1')
    # flux = share.tab_to_flux('Sheet1')

    if iter_method == 'slow':
        ws = lev.sheet

        for r in range(2, lev.last_r + 1):
            if r % 100 == 0:
                print('at row: {:,}'.format(r))

            ws.Range('C' + str(r)).Value = 'blah'
            if ws.Range('B' + str(r)).Value == 'find':
                ws.Range('B' + str(r)).Value = 'replace'

    else:
        m = list(lev)
        for row in m:
            if row.col_b == 'find':
                row.col_b = 'replace'

        lev['*f *f'] = m


def excel_object_model():
    from vengeance.excel_com.worksheet import activate_sheet
    from vengeance.excel_com.worksheet import clear_worksheet_filter

    ws = veng.get_worksheet(share.wb, 'object model')
    activate_sheet(ws)
    ws.Range('B2:D10').Interior.Color = xlNone

    clear_worksheet_filter(ws)

    ws.Range('B2:B10').Interior.Color = xlYellow
    ws.Range('C2:C10').Interior.Color = xlBlue
    ws.Range('D2:D10').Interior.Color = xlPink

    lev_1 = share.tab_to_lev('Sheet1')
    lev_2 = share.tab_to_lev('object model')

    lev_2.remove_filter()
    lev_2.reapply_filter()

    # special formatting
    lev_2['I *h:K *h'].ColumnWidth = 1

    lev_1['*f *h:*l *h'].Copy()
    lev_2['I *h'].PasteSpecial(Paste=xlPasteColumnWidths,
                               Operation=xlNone,
                               SkipBlanks=False,
                               Transpose=False)

    lev_1.application.CutCopyMode = False

    # invoke macro
    path = "'{}'".format(share.wb.FullName)
    macro_name = 'msgbox_test'
    share.wb.Application.Run(path + '!' + macro_name, 'hello from python')


def allow_focus():
    """
    if excel_levity_cls.allow_focus = False, lev.activate() will have no effect

    setting this value to False allows processes to run without disrupting the user
    """
    print()
    activate_all_sheets()

    print()

    veng.excel_levity_cls.allow_focus = True
    activate_all_sheets()


def activate_all_sheets():
    print('veng.excel_levity_cls.allow_focus = {}'.format(veng.excel_levity_cls.allow_focus))

    for ws in share.wb.Sheets:
        print("activate: '{}'".format(ws.Name))
        lev = share.tab_to_lev(ws, clear_filter=True)
        lev.activate()

        sleep(0.25)


main()

