#!/usr/bin/env python3
# Copyright (c) 2020 Project CHIP Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Flash an ESP32 device.

This is layered so that a caller can perform individual operations
through an `Flasher` instance, or operations according to a command line.
For `Flasher`, see the class documentation. For the parse_command()
interface or standalone execution:

usage: esp32_firmware_utils.py [-h] [--verbose] [--erase] [--application FILE]
                               [--verify_application] [--reset] [--skip_reset]
                               [--esptool FILE] [--parttool FILE]
                               [--sdkconfig FILE] [--chip CHIP] [--port PORT]
                               [--baud BAUD] [--before ACTION]
                               [--after ACTION] [--flash_mode MODE]
                               [--flash_freq FREQ] [--flash_size SIZE]
                               [--compress] [--bootloader FILE]
                               [--bootloader_offset OFFSET] [--partition FILE]
                               [--partition_offset OFFSET]
                               [--application_offset OFFSET]

Flash ESP32 device

optional arguments:
  -h, --help            show this help message and exit

configuration:
  --verbose, -v         Report more verbosely
  --esptool FILE        File name of the esptool executable
  --parttool FILE       File name of the parttool executable
  --sdkconfig FILE      File containing option defaults
  --chip CHIP           Target chip type
  --port PORT           Serial port device
  --baud BAUD           Serial port baud rate
  --before ACTION       What to do before connecting
  --after ACTION        What to do when finished
  --flash_mode MODE, --flash-mode MODE
                        Flash mode
  --flash_freq FREQ, --flash-freq FREQ
                        Flash frequency
  --flash_size SIZE, --flash-size SIZE
                        Flash size
  --compress, -z        Compress data in transfer
  --bootloader FILE     Bootloader image
  --bootloader_offset OFFSET, --bootloader-offset OFFSET
                        Bootloader offset
  --partition FILE      Partition table image
  --partition_offset OFFSET, --partition-offset OFFSET
                        Partition table offset
  --application_offset OFFSET, --application-offset OFFSET
                        Application offset

operations:
  --erase               Erase device
  --application FILE    Flash an image
  --verify_application, --verify-application
                        Verify the image after flashing
  --reset               Reset device after flashing
  --skip_reset, --skip-reset
                        Do not reset device after flashing
"""

import os
import pathlib
import sys

import firmware_utils

# Additional options that can be use to configure an `Flasher`
# object (as dictionary keys) and/or passed as command line options.
ESP32_OPTIONS = {
    # Configuration options define properties used in flashing operations.
    'configuration': {
        # Tool configuration options.
        'esptool': {
            'help': 'File name of the esptool executable',
            'default': None,
            'argparse': {
                'metavar': 'FILE',
            },
            'command': [
                {'option': 'esptool'},
                {'optional': 'port'},
                {'optional': 'baud'},
                {'optional': 'before'},
                {'optional': 'after'},
                ()
            ],
            'verify': ['{esptool}', 'version'],
            'error':
                """\
                Unable to execute {esptool}.

                Please ensure that this tool is installed and
                that $IDF_PATH is set. See the ESP32 example
                README for installation instructions.
                """,
        },
        'parttool': {
            'help': 'File name of the parttool executable',
            'default': None,
            'argparse': {
                'metavar': 'FILE'
            },
            'command': [
                {'option': 'parttool'},
                {'optional': 'port'},
                {'optional': 'baud'},
                {
                    'option': 'partition',
                    'result': ['--partition-table-file', '{partition}'],
                    'expand': True
                },
                ()
            ],
            'verify': ['{parttool}', '--quiet'],
            'error':
                """\
                Unable to execute {parttool}.

                Please ensure that this tool is installed and
                that $IDF_PATH is set. See the ESP32 example
                README for installation instructions.
                """,
        },
        'sdkconfig': {
            'help': 'File containing option defaults',
            'default': None,
            'argparse': {
                'metavar': 'FILE'
            },
        },

        # Device configuration options.
        'chip': {
            'help': 'Target chip type',
            'default': 'esp32',
            'argparse': {
                'metavar': 'CHIP'
            },
            'sdkconfig': 'CONFIG_IDF_TARGET',
        },
        'port': {
            'help': 'Serial port device',
            'default': None,
            'argparse': {
                'metavar': 'PORT',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_PORT',
        },
        'baud': {
            'help': 'Serial port baud rate',
            'default': None,
            'argparse': {
                'metavar': 'BAUD',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_BAUD',
        },
        'before': {
            'help': 'What to do before connecting',
            'default': None,
            'argparse': {
                'metavar': 'ACTION',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_BEFORE',
        },
        'after': {
            'help': 'What to do when finished',
            'default': None,
            'argparse': {
                'metavar': 'ACTION',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_AFTER',
        },
        'flash_mode': {
            'help': 'Flash mode',
            'default': None,
            'argparse': {
                'metavar': 'MODE',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHMODE',
        },
        'flash_freq': {
            'help': 'Flash frequency',
            'default': None,
            'argparse': {
                'metavar': 'FREQ',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHFREQ',
        },
        'flash_size': {
            'help': 'Flash size',
            'default': None,
            'argparse': {
                'metavar': 'SIZE',
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_FLASHSIZE',
        },
        'compress': {
            'help': 'Compress data in transfer',
            'default': None,
            'alias': ['-z'],
            'argparse': {
                'action': 'store_true'
            },
            'sdkconfig': 'CONFIG_ESPTOOLPY_COMPRESSED',
        },

        # Flashing things.
        'bootloader': {
            'help': 'Bootloader image',
            'default': None,
            'argparse': {
                'metavar': 'FILE',
                'type': pathlib.Path,
            },
        },
        'bootloader_offset': {
            'help': 'Bootloader offset',
            'default': '0x1000',
            'argparse': {
                'metavar': 'OFFSET'
            },
            'sdkconfig': 'CONFIG_BOOTLOADER_OFFSET_IN_FLASH',
        },
        'partition': {
            'help': 'Partition table image',
            'default': None,
            'argparse': {
                'metavar': 'FILE',
                'type': pathlib.Path,
            },
        },
        'partition_offset': {
            'help': 'Partition table offset',
            'default': None,
            'argparse': {
                'metavar': 'OFFSET'
            },
            'sdkconfig': 'CONFIG_PARTITION_TABLE_OFFSET',
        },
        'application_offset': {
            'help': 'Application offset',
            'default': None,
            'argparse': {
                'metavar': 'OFFSET'
            },
        },
    },
}


def namespace_defaults(dst, src):
    for key, value in src.items():
        if key not in dst or getattr(dst, key) is None:
            setattr(dst, key, value)


class Flasher(firmware_utils.Flasher):
    """Manage esp32 flashing."""

    def __init__(self, **options):
        super().__init__(platform='ESP32', module=__name__, **options)
        self.define_options(ESP32_OPTIONS)

    def _postprocess_argv(self):
        if self.option.sdkconfig:
            namespace_defaults(self.option,
                               self.read_sdkconfig(self.option.sdkconfig))
#       idf = os.environ.get('IDF_PATH')
#       if idf:
#           if self.option.esptool is None:
#               self.option.esptool = os.path.join(
#                   idf,
#                   'components/esptool_py/esptool/esptool.py')
#           if self.option.parttool is None:
#               self.option.parttool = os.path.join(
#                   idf,
#                   'components/partition_table/parttool.py')

    def read_sdkconfig(self, filename):
        """Given an ESP32 sdkconfig file, read it for values of options
           not otherwise set.
        """
        config_map = {}
        for key, info in vars(self.info).items():
            config_key = info.get('sdkconfig')
            if config_key:
                config_map[config_key] = key
        result = {}
        with open(filename) as f:
            for line in f:
                k, eq, v = line.strip().partition('=')
                if eq == '=' and k in config_map:
                    result[config_map[k]] = v.strip('"')
        return result

    IDF_PATH_TOOLS = {
        'esptool': 'components/esptool_py/esptool/esptool.py',
        'parttool': 'components/partition_table/parttool.py',
    }

    def locate_tool(self, tool):
        if tool in self.IDF_PATH_TOOLS:
            idf_path = os.environ.get('IDF_PATH')
            if idf_path:
                return os.path.join(idf_path, self.IDF_PATH_TOOLS[tool])
        return super().locate_tool(tool)

    # Common command line arguments for esptool flashing subcommands.
    FLASH_ARGUMENTS = [
        {'optional': 'flash_mode'},
        {'optional': 'flash_freq'},
        {'optional': 'flash_size'},
        {
            'match': '{compress}',
            'test': [(True, '-z'), ('y', '-z')],
            'default': '-u'
        },
    ]

    def erase(self):
        """Perform `commander device masserase`."""
        return self.run_tool('esptool', ['erase_flash'], {}, 'Erase device')

    def verify(self, image, address=0):
        """Verify image(s)."""
        if not isinstance(image, list):
            image = [address, image]
        return self.run_tool(
            'esptool',
            ['verify_flash', self.FLASH_ARGUMENTS, image],
            name='Verify',
            pass_message='Verified',
            fail_message='Not verified',
            fail_level=2)

    def flash(self, image, address=0):
        """Flash image(s)."""
        if not isinstance(image, list):
            image = [address, image]
        return self.run_tool(
            'esptool',
            ['write_flash', self.FLASH_ARGUMENTS, image],
            name='Flash')

    PARTITION_INFO = {
        'phy': ['--partition-type', 'data', '--partition-subtype', 'phy'],
        'application': ['--partition-boot-default'],
        'ota': ['--partition-type', 'data', '--partition-subtype', 'ota'],
        'factory': [
            '--partition-type', 'app', '--partition-subtype', 'factory'
        ],
    }

    def get_partition_info(self, item, info, options=None):
        """Run parttool to get partition information."""
        return self.run_tool(
            'parttool',
            ['get_partition_info', self.PARTITION_INFO[item], '--info', info],
            options or {},
            capture_output=True).stdout.strip()

    def make_wrapper(self, argv):
        self.parser.add_argument(
            '--use-parttool',
            metavar='FILENAME',
            help='partition tool to configure flashing script')
        self.parser.add_argument(
            '--use-partition-file',
            metavar='FILENAME',
            help='partition file to configure flashing script')
        self.parser.add_argument(
            '--use-sdkconfig',
            metavar='FILENAME',
            help='sdkconfig to configure flashing script')
        super().make_wrapper(argv)

    def _platform_wrapper_args(self, args):
        if args.use_sdkconfig:
            # Include values from sdkconfig so that it isn't needed at
            # flashing time.
            namespace_defaults(args, self.read_sdkconfig(args.use_sdkconfig))
        parttool = args.use_parttool or args.parttool
        partfile = args.use_partition_file or args.partition
        if parttool and partfile:
            # Get unspecified offsets from the partition file now,
            # so that parttool isn't needed at flashing time.
            if args.application and args.application_offset is None:
                args.application_offset = self.get_partition_info(
                    'application', 'offset',
                    {'parttool': parttool, 'partition': partfile})

    def actions(self):
        """Perform actions on the device according to self.option."""
        self.log(3, 'Options:', self.option)

        if self.option.erase:
            if self.erase().err:
                return self

        if self.option.application:
            application = self.option.application
            bootloader = self.option.bootloader
            partition = self.option.partition

            # Collect the flashable items.
            flash = []
            if bootloader:
                flash += [self.option.bootloader_offset, bootloader]
            if application:
                offset = self.option.application_offset
                if offset is None:
                    offset = self.get_partition_info('application', 'offset')
                flash += [offset, application]
            if partition:
                flash += [self.option.partition_offset, partition]

            # esptool.py doesn't have an independent reset command, so we add
            # an `--after` option to the final operation, which may be either
            # flash or verify.
            if self.option.after is None:
                if self.option.reset or self.option.reset is None:
                    self.option.after = 'hard_reset'
                else:
                    self.option.after = 'no_reset'
            if self.option.verify_application:
                verify_after = self.option.after
                self.option.after = 'no_reset'

            if self.flash(flash).err:
                return self
            if self.option.verify_application:
                self.option.after = verify_after
                if self.verify(flash).err:
                    return self

        return self

# Mobly integration


class ESP32Platform:
    def __init__(self, flasher_args):
        self.flasher = Flasher(**flasher_args)

    def flash(self):
        self.flasher.flash_command([os.getcwd()])


def verify_platform_args(platform_args):
    required_args = [
        'application',
        'parttool',
        'port',
        'baud',
        'before',
        'after',
        'flash_mode',
        'flash_freq',
        'flash_size',
        'compress',
        'bootloader',
        'partition',
        'partition_offset',
        'application_offset',
    ]
    difference = set(required_args) - set(platform_args)
    if difference:
        raise ValueError("Required arguments missing: %s" % difference)


def create_platform(platform_args):
    verify_platform_args(platform_args[0])
    return ESP32Platform(platform_args[0])

# End of Mobly integration


if __name__ == '__main__':
    sys.exit(Flasher().flash_command(sys.argv))
