import datetime
from typing import List

from scrilla.static import constants, formats, definitions


### GENERAL OUTPUT FUNCTIONS
def space(n: int):
    print('\n'*n)

def title_line(title: str, line_length: int=formats.formats['LINE_LENGTH'], separator: str=formats.formats['separator'],
display: bool=True):
    buff = int((line_length - len(title))/2)
    result = separator*buff + title + separator*buff
    if display:
        print(result)
    return result 
    
def separator_line(line_length=formats.formats['LINE_LENGTH'], separator=formats.formats['separator'], display=True) -> str:
    if display:
        print(separator*line_length)
    return separator*line_length

def break_lines(msg: str, line_length: int=formats.formats['LINE_LENGTH']) -> List[str]:
    """
    Generates a list of strings where each string is less than or equal to a specified line length. All elements in the array will be equal length except the last element; in other words, the inputted string will be read into an array, line by line, until there is nothing left to be read.

    Parameters
    ----------
    1. ``msg``: ``str``
        String to be broken into an array with each string no longer than `line_length`.
    2. **line_length**: ``int``
        *Optional*. Defaults to the value set by `static.formats.formats['LINE_LENGTH]`. The maximum length of a line. 
    
    Returns
    -------
    `list[str]`
        A list of `line`'s with `len(line)<line_length or len(line)==line_length`.
    """
    
    if len(msg)>line_length:
        return [msg[i:i+line_length] for i in range(0,len(msg), line_length)]
    return [msg]

def center(this_line: str, line_length: int=formats.formats['LINE_LENGTH'], display: bool=True) -> str:
    """
    Centers an inputted line relative to the line length. 

    Parameters
    ----------
    1. **this_line**: ``str``
        the string to centered.
    2. **line_length**: ``int``
        *Optional*. Defaults to the value set by `static.formats.formats['LINE_LENGTH]`. The length of the line that is being centered over. 
    3. **display**: ``bool``
        *Optional*. Defaults to `True`. If set to `True`, function will print result to screen. If set to `False`, the function will return the value as a string.
    
    Returns
    -------
    `str`
        The line with the difference between `line_length` and `this_line` distributed evenly on either side of the content as white space. 

    Raises
    ------
    1. **ValueError**
        If the inputted line is larged than the line length, this error will be thrown.
    """
    
    if len(this_line)>line_length:
        raise ValueError('Line to be centered is larger than length being centered over.')

    buff = int((line_length - len(this_line))/2)
    output = ' '*buff +  this_line + ' '*buff
    if display:
        print(output)
        return
    return output

def print_list(list_to_print, indent=formats.formats['INDENT']):
    for i, item in enumerate(list_to_print):
        print(' '*indent, f'{i}. {item}')

def string_result(operation, result, indent=formats.formats['INDENT'], display=True):
    output = ' '*indent, operation, ' = ', result
    if display:
        print(output)
        return
    return output

def scalar_result(calculation, result, currency=True, indent=formats.formats['INDENT']):
    if currency:
        print(' '*indent, calculation, ' = $', round(float(result), 2))
    else:
        print(' '*indent, calculation, ' = ', round(float(result), 4))

def percent_result(calculation, result, indent=formats.formats['INDENT']):
    print(' '*indent, calculation, ' = ', round(float(result), 4), '%')

def equivalent_result(right_hand, left_hand, value, indent=formats.formats['INDENT']):
    print(' '*indent, f'{right_hand} = {left_hand} = {value}')

def help_msg(indent=formats.formats['INDENT']):
    func_dict, arg_dict = definitions.FUNC_DICT, definitions.ARG_DICT

    title_line('scrilla')
    space(1)

    for paragraph in definitions.HELP_MSG:
        for line in break_lines(paragraph):
            center(line)
        space(1)

    title_line('SYNTAX')
    center(definitions.SYNTAX)
    space(1)

    for func_name in func_dict:
        title_line(func_dict[func_name]['name'])
        for line in break_lines(func_dict[func_name]['description']):
            center(line)
        separator_line()

        commands = func_dict[func_name]['values']
        print(' ', f'COMMAND: {commands[0]}, {commands[1]}')


        if func_dict[func_name]['args'] is not None:
            for arg_name in func_dict[func_name]['args']:
                aliases = arg_dict[arg_name]['values']

                print(' '*indent, f'OPTION: {aliases[0]}, {aliases[1]}, {aliases[2]}, {aliases[3]}')

                if arg_dict[arg_name]['required']:
                    print(' '*2*indent, 'REQUIRED')

                print(' '*2*indent, f'NAME: {arg_dict[arg_name]["name"]}')

                if arg_dict[arg_name]['default'] is not None:
                    print(' '*2*indent, f'DEFAULT: {arg_dict[arg_name]["default"]}')
                    
                if arg_dict[arg_name]['syntax'] is not None:
                    print(' '*2*indent, f'FORMAT: {arg_dict[arg_name]["syntax"]}')    
        separator_line()    

### ANALYSIS SPECIFIC OUTPUT FUNCTIONS
def portfolio_percent_result(result, tickers,indent=formats.formats['INDENT']):
    for i, item in enumerate(tickers):
        print(' '*indent, f'{item} =', round(100*result[i], 2), '%')

def portfolio_shares_result(result, tickers, indent=formats.formats['INDENT']):
    for i, item in enumerate(tickers):
        print(' '*indent, f'{item} =', result[i])

def spot_price(ticker, this_spot_price):
    formatted_price = round(float(this_spot_price), 2)
    scalar_result(f'{ticker} spot price', formatted_price)
    
def model_price(ticker, this_model_price, model):
    formatted_price = round(float(this_model_price),2)
    scalar_result(f'{ticker} {str(model).upper()} price', formatted_price)

def risk_profile(profiles):
    for key, value in profiles.items():
        title_line(f'{key} Risk Profile')
        for subkey, subvalue in value.items():
            scalar_result(f'{subkey}', f'{subvalue}', currency=False)

def moving_average_result(tickers, averages_output, periods, start_date = None, end_date = None):
    averages, dates = averages_output
    MA1_prefix, MA2_prefix, MA3_prefix = f'MA({periods[0]})', f'MA({periods[1]})', f'MA({periods[2]})'
    if start_date is None and end_date is None:
        for i, item in enumerate(tickers):
            title = f'{item} Moving Average of Daily Return for {periods[0]}, {periods[1]} & {periods[0]} Days'
            title_line(title)

            MA1_title, MA2_title, MA3_title = f'{MA1_prefix}_{item}', f'{MA2_prefix}_{item}', f'{MA3_prefix}_{item}'
            scalar_result(MA1_title, round(averages[i][0], 2))
            scalar_result(MA2_title, round(averages[i][1], 2))
            scalar_result(MA3_title, round(averages[i][2], 2))
    else:
        for i, item in enumerate(tickers):

            title = f'{item} Moving Average of Daily Return for {periods[0]}, {periods[1]} & {periods[0]} Days'
            title_line(title)

            MA1_title, MA2_title, MA3_title = f'{MA1_prefix}_{item}', f'{MA2_prefix}_{item}', f'{MA3_prefix}_{item}'
            for j, item in enumerate(dates):
                msg_1 = f'{item} : {MA1_title}'
                scalar_result(msg_1, round(averages[i][0][j], 2))
            for j, item in enumerate(dates):
                msg_2 = f'{item} : {MA2_title}'
                scalar_result(msg_2, round(averages[i][1][j], 2))
            for j, item in enumerate(dates):  
                msg_3 = f'{item} : {MA3_title}'
                scalar_result(msg_3, round(averages[i][2][j], 2))      

def screen_results(info, model):
    for ticker in info:
        title_line(f'{ticker} {str(model).upper()} Model vs. Spot Price ')
        spot_price(ticker=ticker, this_spot_price=info[ticker]['spot_price'])
        model_price(ticker=ticker, this_model_price=info[ticker]['model_price'], model=model)
        scalar_result(f'{ticker} discount', info[ticker]['discount'])
        separator_line()

# TODO: can probably combine optimal_result and efficient_frontier into a single function
#         by wrapping the optimal_results in an array so when it iterates through frontier
#         in efficient_frontier, it will only pick up the single allocation array for the
#         optimal result.
def optimal_result(portfolio, allocation, investment=None):
    title_line('Optimal Percentage Allocation')
    portfolio_percent_result(allocation, portfolio.tickers)

    if investment is not None:
        shares = portfolio.calculate_approximate_shares(allocation, investment)
        total = portfolio.calculate_actual_total(allocation, investment)
        
        title_line('Optimal Share Allocation')
        portfolio_shares_result(shares, portfolio.tickers)
        title_line('Optimal Portfolio Value')
        scalar_result('Total', round(total,2))

    title_line('Risk-Return Profile')
    scalar_result(calculation='Return', result=portfolio.return_function(allocation), currency=False)
    scalar_result(calculation='Volatility', result=portfolio.volatility_function(allocation), currency=False)

def efficient_frontier(portfolio, frontier, investment=None):
    title_line('(Annual Return %, Annual Volatility %) Portfolio')

    # TODO: edit title to include dates

    for allocation in frontier:
        separator_line()
        return_string=str(round(round(portfolio.return_function(allocation),4)*100,2))
        vol_string=str(round(round(portfolio.volatility_function(allocation),4)*100,2))
        title_line(f'({return_string} %, {vol_string}%) Portfolio')
        separator_line()

        title_line('Optimal Percentage Allocation')
        portfolio_percent_result(allocation, portfolio.tickers)
        
        if investment is not None:
            shares = portfolio.calculate_approximate_shares(allocation, investment)
            total = portfolio.calculate_actual_total(allocation, investment)
        
            title_line('Optimal Share Allocation')
            portfolio_shares_result(shares, portfolio.tickers)
            title_line('Optimal Portfolio Value')
            scalar_result('Total', round(total,2))
        
        title_line('Risk-Return Profile')
        scalar_result('Return', portfolio.return_function(allocation), currency=False)
        scalar_result('Volatility', portfolio.volatility_function(allocation), currency=False)
        print('\n')

def correlation_matrix(tickers: List[str], correlation_matrix: List[List[float]], display: bool = True):
    """
    Parameters
    ----------
    1. **tickers**: ``list``
        Array of tickers for which the correlation matrix was calculated and formatted.
    2. **indent**: ``int``
        Amount of indent on each new line of the correlation matrix.
    3. **start_date**: ``datetime.date`` 
        Start date of the time period over which correlation was calculated. 
    4. **end_date**: ``datetime.date`` 
        End date of the time period over which correlation was calculated. 
    
    Returns
    ------
    A correlation matrix string formatted with new lines and spaces.
    """
    entire_formatted_result, formatted_title = "", ""

    line_length, first_symbol_length = 0, 0
    new_line=""
    no_symbols = len(tickers)

    for i in range(no_symbols):
        this_symbol = tickers[i]
        symbol_string = ' '*formats.formats['INDENT'] + f'{this_symbol} '

        if i != 0:
            this_line = symbol_string + ' '*(line_length - len(symbol_string) - 7*(no_symbols - i))
            # NOTE: seven is number of chars in ' 100.0%'
        else: 
            this_line = symbol_string
            first_symbol_length = len(this_symbol)

        new_line = this_line
        
        for j in range(i, no_symbols):
            if j == i:
                new_line += " 100.0%"
            
            else:
                result = correlation_matrix[i][j]
                formatted_result = str(100*result)[:constants.constants['SIG_FIGS']]
                new_line += f' {formatted_result}%'

        entire_formatted_result += new_line + '\n'
        
        if i == 0:
            line_length = len(new_line)

    formatted_title += ' '*(formats.formats['INDENT'] + first_symbol_length+1)
    for symbol in tickers:
        sym_len = len(symbol)
        formatted_title += f' {symbol}'+ ' '*(7-sym_len)
        # NOTE: seven is number of chars in ' 100.0%'
    formatted_title += '\n'

    whole_thing = formatted_title + entire_formatted_result

    if display:
        print(f'\n{whole_thing}')
        return

    return whole_thing

class Logger():

    def __init__(self, location, log_level="info"):
        self.location = location
        self.log_level = log_level
    
    # LOGGING FUNCTIONS
    def comment(self, msg):
        now = datetime.datetime.now()
        dt_string = now.strftime("%d/%m/%Y %H:%M:%S")
        print(dt_string, ' :' , self.location, ' : ',msg)

    def info(self, msg):
        if self.log_level in [constants.constants['LOG_LEVEL']['INFO'], 
                                constants.constants['LOG_LEVEL']['DEBUG'], 
                                constants.constants['LOG_LEVEL']['VERBOSE']]:
            self.comment(msg)

    def debug(self, msg):
        if self.log_level in [constants.constants['LOG_LEVEL']['DEBUG'], 
                                constants.constants['LOG_LEVEL']['VERBOSE']]:
            self.comment(msg)

    def verbose(self, msg):
        if self.log_level == constants.constants['LOG_LEVEL']['VERBOSE']:
            self.comment(msg)
