#!/usr/bin/env python3
"""Dump the log file analysis reports generated by the VASSAL shim."""

import os
import itertools

import click
import tabulate

from vasl_templates.webapp.lfa import parse_analysis_report, DEFAULT_LFA_DICE_HOTNESS_WEIGHTS

EXPECTED_DISTRIB = {
    "DR": { 2: 2.8, 3: 5.6, 4: 8.3, 5: 11.1, 6: 13.9, 7: 16.7, 8: 13.9, 9: 11.1, 10: 8.3, 11: 5.6, 12: 2.8 },
    "dr": { 1: 16.7, 2: 16.7, 3: 16.7, 4: 16.7, 5: 16.7, 6: 16.7 },
}

# ---------------------------------------------------------------------

@click.command()
@click.option( "--file","-f","fname", required=True, help="Log file analysis report." )
@click.option( "--players/--no-players","-p",  help="Dump the players." )
@click.option( "--events/--no-events","-e",  help="Dump the events." )
@click.option( "--roll-type","-r","roll_type", help="Roll type filter (e.g. IFT or MC)." )
@click.option( "--window","-w","window_size", default=1, help="Moving average window size." )
def main( fname, players, events, roll_type, window_size ):
    """Dump a Log File Analysis report (generated by the VASSAL shim)."""

    # initialize
    if not os.path.isfile( fname ):
        raise RuntimeError( "Can't find the report file: {}".format( fname ) )

    # parse the report
    report = parse_analysis_report( fname )

    # dump each log file
    for log_file in report["logFiles"]:

        # output a header for the next log file
        print( "=== {} {}".format( log_file["filename"], 80*"=" )[ :80 ] )
        print()

        # dump the scenario details
        scenario_name = log_file["scenario"].get( "scenarioName" )
        if scenario_name:
            print( "Scenario: {}".format( scenario_name ), end="" )
            scenario_id = log_file["scenario"].get( "scenarioId" )
            if scenario_id:
                print( " ({})".format( scenario_id ), end="" )
            print()

        # dump the players
        if players:
            print( "Players:" )
            max_id_len = max( len(k) for k in report["players"] )
            fmt = "- {:%d} = {}" % max_id_len
            for player_id,player_name in report["players"].items():
                print( fmt.format( player_id, player_name ) )

        # dump the DR/dr distributions
        dump_distrib( report["players"], log_file, roll_type )
        print()

        # dump the time-plot
        if events or roll_type:
            dump_time_plot( report["players"], log_file, roll_type, window_size )

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def dump_distrib( players, log_file, roll_type ): #pylint: disable=too-many-locals,too-many-branches,too-many-statements
    """Dump the DR/dr distributions."""

    # initialize
    stats = { p: {
        "DR": { "nRolls": 0, "rollTotal": 0 },
        "dr": { "nRolls": 0, "rollTotal": 0 },
        } for p in players
    }
    distrib = { p: {
        "DR": { k: 0 for k in range(2,12+1) },
        "dr": { k: 0 for k in range(1,6+1) },
        } for p in players
    }

    # process events
    for evt in log_file["events"]:

        # check if we should process the next event
        if evt["eventType"] != "roll":
            continue
        if roll_type and evt["rollType"].lower() != roll_type.lower():
            continue

        # update the stats
        player_id = evt["playerId"]
        key = "DR" if isinstance( evt["rollValue"], list ) else "dr"
        stats[ player_id ][ key ][ "nRolls" ] += 1
        val = roll_total( evt["rollValue"] )
        stats[ player_id ][ key ][ "rollTotal" ] += val
        distrib[ player_id ][ key ][ val ] += 1

    # calculate averages
    avg = lambda x, y: x / y if y != 0 else 0
    for player_id in players:
        for key in ["DR","dr"]:
            stats[ player_id ][ key ][ "rollAverage" ] = avg(
                stats[player_id][key].pop("rollTotal"),
                stats[player_id][key]["nRolls"]
            )

    # calculate chi-squared and hotness
    for player_id in players:
        for key in ["DR","dr"]:
            stats[ player_id ][ key ][ "chiSquared" ] = chi_squared(
                distrib[player_id][ key ],
                EXPECTED_DISTRIB[ key ]
            )
            stats[ player_id ][ key ][ "hotness" ] = hotness(
                distrib[player_id][ key ],
                EXPECTED_DISTRIB[ key ],
                DEFAULT_LFA_DICE_HOTNESS_WEIGHTS[ key ],
            )

    # output the results
    for key in ["dr","DR"]:
        print()
        print( "=== {} distribution ===".format( key ) )
        vals = range(2,12+1) if key == "DR" else range(1,6+1)
        results = [ itertools.chain( [""], vals, ["total","average","chi2","hotness"] ) ]
        total_rolls = sum( stats[p][key]["nRolls"] for p in players )
        for player_id,player_name in players.items():
            # add a row for the stats
            row = [ player_name ]
            has_vals = False
            for val in vals:
                nRolls = distrib[player_id][key][val]
                if nRolls != 0:
                    row.append( nRolls )
                    has_vals = True
                else:
                    row.append( "" )
            val2 = stats[player_id][key]["nRolls"]
            val2a = val2 / total_rolls if total_rolls != 0 else 0
            row.append( "{} ({}%)".format( val2, int(100*val2a+0.5) ) )
            row.append( fpfmt( stats[player_id][key]["rollAverage"], 1 ) )
            row.append( fpfmt( stats[player_id][key]["chiSquared"], 3 ) )
            results.append( row )
            # add a row for the averages
            if has_vals:
                row = [ "" ]
                for val in vals:
                    nRolls = distrib[player_id][key][val]
                    if nRolls:
                        val2 = avg( distrib[player_id][key][val], stats[player_id][key]["nRolls"] )
                        row.append( fpfmt( 100*val2, 1 ) )
                    else:
                        row.append( "" )
                results.append( row )
            # add a row for the dice hotness
            row = [ "" ]
            partials = stats[ player_id ][ key ][ "hotness" ]
            if partials:
                for val in partials:
                    row.append( fpfmt( val, 3 ) )
                row.extend( [ "", "", "" ] )
                row.append( fpfmt( sum(partials), 3 ) )
                results.append( row )
        print( tabulate.tabulate( results, headers="firstrow" ) )

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def dump_time_plot( players, log_file, roll_type, window_size ):
    """Dump the time-plot values."""

    # initialize
    rolls = []
    windows = { p: [] for p in players }

    def dump_rolls():
        """Dump the buffered ROLL events."""
        print( tabulate.tabulate( rolls, tablefmt="plain" ) )

    def onTurnTrack( evt ): #pylint: disable=unused-variable,possibly-unused-variable
        """Process a TURN TRACK event."""
        nonlocal rolls
        if rolls:
            dump_rolls()
            rolls = []
            print()
        print( "--- {} Turn {} {} ---".format( evt["side"], evt["turnNo"], evt["phase"] ) )
        print()

    def onRoll( evt ) : #pylint: disable=unused-variable,possibly-unused-variable
        """Process a ROLL event"""
        # check if we should process this ROLL event
        if roll_type:
            if evt["rollType"].lower() != roll_type.lower():
                return
        player_id = evt[ "playerId" ]
        if window_size == 1:
            # add the raw roll
            if isinstance( evt["rollValue"], int ):
                val = evt["rollValue"]
            else:
                val = ", ".join( str(v) for v in evt["rollValue"] )
            rolls.append( [ players[player_id], evt["rollType"], val ] )
        else:
            # add the moving average
            windows[ player_id ].append( roll_total( evt["rollValue"] ) )
            if len(windows[player_id]) < window_size:
                return
            val = sum( windows[player_id] ) / len(windows[player_id])
            del windows[player_id][0]
            rolls.append( [ players[player_id], val ] )

    # process events
    print( "=== EVENTS ===" )
    print()
    for evt in log_file["events"]:
        eventType = evt["eventType"]
        locals()[ "on" + eventType[0].upper() + eventType[1:] ]( evt )
    if rolls:
        dump_rolls()

# ---------------------------------------------------------------------

def chi_squared( observed, expected ):
    """Calculate the chi-squared for a set of values."""
    nRolls = sum( observed.values() )
    if nRolls == 0:
        return None
    assert observed.keys() == expected.keys()
    return sum(
        ( observed[val]/nRolls - expected[val]/100 ) ** 2 / (expected[val]/100)
        for val in expected
    )

def hotness( observed, expected, weights ):
    """Calculate the hotness for a set of values."""
    nRolls = sum( observed.values() )
    if nRolls == 0:
        return None
    assert observed.keys() == expected.keys() == weights.keys()
    partials = []
    sign = lambda val: -1 if val < 0 else +1
    for val in expected:
        diff = observed[val]/nRolls - expected[val]/100
        partials.append( sign(diff) * diff**2 * weights[val] / (expected[val]/100) )
    return partials

def roll_total( roll ):
    """Calculate the total of a roll."""
    if isinstance( roll, list ):
        assert all( isinstance(r,int) for r in roll )
        return sum( roll )
    else:
        assert isinstance( roll, int )
        return roll

def fpfmt( val, nDigits ):
    """Format a floating point number."""
    if val is None:
        return "-"
    return ("{:.%df}" % nDigits).format( val )

# ---------------------------------------------------------------------

if __name__ == "__main__":
    main() #pylint: disable=no-value-for-parameter
