#!/usr/bin/env python
# coding=utf-8
# -------------------------------------------------------------------------------
# ESPurna module memory analyser
# xose.perez@gmail.com
#
# Based on:
# https://github.com/letscontrolit/ESPEasy/blob/mega/memanalyzer.py
# by psy0rz <edwin@datux.nl>
# https://raw.githubusercontent.com/SmingHub/Sming/develop/tools/memanalyzer.py
# by Slavey Karadzhov <slav@attachix.com>
# https://github.com/Sermus/ESP8266_memory_analyzer
# by Andrey Filimonov
#
# -------------------------------------------------------------------------------
from __future__ import print_function

import argparse
import os
import re
import shlex
import subprocess
import sys
from collections import OrderedDict

from sortedcontainers import SortedDict

if sys.version_info > (3, 0):
    from subprocess import getstatusoutput
else:
    from commands import getstatusoutput

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

TOTAL_IRAM = 32786
TOTAL_DRAM = 81920
env = "esp8266-4m-ota"
objdump_binary = "xtensa-lx106-elf-objdump"
sections = OrderedDict([
    ("data", "Initialized Data (RAM)"),
    ("rodata", "ReadOnly Data (RAM)"),
    ("bss", "Uninitialized Data (RAM)"),
    ("text", "Cached Code (IRAM)"),
    ("irom0_text", "Uncached Code (SPI)")
])
description = "ESPurna Memory Analyzer v0.1"


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

def file_size(file):
    try:
        return os.stat(file).st_size
    except OSError:
        return 0


def analyse_memory(elf_file):
    command = "{} -t '{}'".format(objdump_binary, elf_file)
    response = subprocess.check_output(shlex.split(command))
    if isinstance(response, bytes):
        response = response.decode('utf-8')
    lines = response.split('\n')

    # print("{0: >10}|{1: >30}|{2: >12}|{3: >12}|{4: >8}".format("Section", "Description", "Start (hex)", "End (hex)", "Used space"));
    # print("------------------------------------------------------------------------------");
    ret = {}

    for (id_, _) in list(sections.items()):
        section_start_token = " _{}_start".format(id_)
        section_end_token = " _{}_end".format(id_)
        section_start = -1
        section_end = -1
        for line in lines:
            if section_start_token in line:
                data = line.split(' ')
                section_start = int(data[0], 16)

            if section_end_token in line:
                data = line.split(' ')
                section_end = int(data[0], 16)

            if section_start != -1 and section_end != -1:
                break

        section_length = section_end - section_start
        # if i < 3:
        #     usedRAM += section_length
        # if i == 3:
        #     usedIRAM = TOTAL_IRAM - section_length;

        ret[id_] = section_length
        # print("{0: >10}|{1: >30}|{2:12X}|{3:12X}|{4:8}".format(id_, descr, section_start, section_end, section_length))
        # i += 1

    # print("Total Used RAM : {:d}".format(usedRAM))
    # print("Free RAM : {:d}".format(TOTAL_DRAM - usedRAM))
    # print("Free IRam : {:d}".format(usedIRAM))
    return ret


def run(env_, modules_):
    flags = ""
    for k, v in modules_.items():
        flags += "-D{}_SUPPORT={:d} ".format(k, v)
    os_env = os.environ.copy()
    os_env["ESPURNA_BOARD"] = "WEMOS_D1_MINI_RELAYSHIELD"
    os_env["ESPURNA_FLAGS"] = flags
    os_env["ESPURNA_PIO_SHARED_LIBRARIES"] = "y"
    command = "platformio run --silent --environment {} 2>/dev/null".format(env_)
    subprocess.check_call(command, shell=True)


def calc_free(module):
    free = 80 * 1024 - module['data'] - module['rodata'] - module['bss']
    free = free + (16 - free % 16)
    module['free'] = free


def modules_get():
    modules_ = SortedDict()
    for line in open("espurna/config/arduino.h"):
        m = re.search(r'(\w*)_SUPPORT', line)
        if m:
            modules_[m.group(1)] = 0
    del modules_['LLMNR']
    del modules_['NETBIOS']
    return modules_


if __name__ == '__main__':

    # Parse command line options
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument("modules", nargs='*', help="Modules to test (use ALL to test them all)")
    parser.add_argument("-c", "--core", help="use core as base configuration instead of default", default=0, action='count')
    parser.add_argument("-l", "--list", help="list available modules", default=0, action='count')
    args = parser.parse_args()

    # Hello
    print()
    print(description)
    print()

    # Check xtensa-lx106-elf-objdump is in the path
    status, result = getstatusoutput(objdump_binary)
    if status != 2 and status != 512:
        print("xtensa-lx106-elf-objdump not found, please check it is in your PATH")
        sys.exit(1)

    # Load list of all modules
    available_modules = modules_get()
    if args.list > 0:
        print("List of available modules:\n")
        for key, value in available_modules.items():
            print("* " + key)
        print()
        sys.exit(0)

    # Which modules to test?
    test_modules = []
    if len(args.modules) > 0:
        if "ALL" in args.modules:
            test_modules = available_modules.keys()
        else:
            test_modules = args.modules

    # Check test modules exist
    for module in test_modules:
        if module not in available_modules:
            print("Module {} not found".format(module))
            sys.exit(2)

    # Define base configuration
    if args.core == 0:
        modules = SortedDict()
        for m in test_modules:
            modules[m] = 0
    else:
        modules = available_modules

    # Show init message
    if len(test_modules) > 0:
        print("Analyzing module(s) {} on top of {} configuration\n".format(", ".join(test_modules), "CORE" if args.core > 0 else "DEFAULT"))
    else:
        print("Analyzing {} configuration\n".format("CORE" if args.core > 0 else "DEFAULT"))

    output_format = "{:<20}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}|{:<15}"
    print(output_format.format(
            "Module",
            "Cache IRAM",
            "Init RAM",
            "R.O. RAM",
            "Uninit RAM",
            "Available RAM",
            "Flash ROM",
            "Binary size"
    ))
    print(output_format.replace("<", ">").format(
            "",
            ".text",
            ".data",
            ".rodata",
            ".bss",
            "heap + stack",
            ".irom0.text",
            ""
    ))
    print(output_format.format(
            "-" * 20,
            "-" * 15,
            "-" * 15,
            "-" * 15,
            "-" * 15,
            "-" * 15,
            "-" * 15,
            "-" * 15
    ))

    # Build the core without modules to get base memory usage
    run(env, modules)
    base = analyse_memory(".pio/build/{}/firmware.elf".format(env))
    base['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
    calc_free(base)
    print(output_format.format(
            "CORE" if args.core == 1 else "DEFAULT",
            base['text'],
            base['data'],
            base['rodata'],
            base['bss'],
            base['free'],
            base['irom0_text'],
            base['size'],
    ))

    # Test each module
    results = {}
    for module in test_modules:

        modules[module] = 1
        run(env, modules)
        results[module] = analyse_memory(".pio/build/{}/firmware.elf".format(env))
        results[module]['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
        calc_free(results[module])
        modules[module] = 0

        print(output_format.format(
                module,
                results[module]['text'] - base['text'],
                results[module]['data'] - base['data'],
                results[module]['rodata'] - base['rodata'],
                results[module]['bss'] - base['bss'],
                results[module]['free'] - base['free'],
                results[module]['irom0_text'] - base['irom0_text'],
                results[module]['size'] - base['size'],
        ))

    # Test all modules
    if len(test_modules) > 0:

        for module in test_modules:
            modules[module] = 1
        run(env, modules)
        total = analyse_memory(".pio/build/{}/firmware.elf".format(env))
        total['size'] = file_size(".pio/build/{}/firmware.bin".format(env))
        calc_free(total)

        print(output_format.format(
                "-" * 20,
                "-" * 15,
                "-" * 15,
                "-" * 15,
                "-" * 15,
                "-" * 15,
                "-" * 15,
                "-" * 15
        ))
        
        if len(test_modules) > 1:
            print(output_format.format(
                    "ALL MODULES",
                    total['text'] - base['text'],
                    total['data'] - base['data'],
                    total['rodata'] - base['rodata'],
                    total['bss'] - base['bss'],
                    total['free'] - base['free'],
                    total['irom0_text'] - base['irom0_text'],
                    total['size'] - base['size'],
            ))

        print(output_format.format(
                "TOTAL",
                total['text'],
                total['data'],
                total['rodata'],
                total['bss'],
                total['free'],
                total['irom0_text'],
                total['size'],
        ))


print("\n")
