#!/usr/bin/env python3

# Author: Phil Elwell <phil@raspberrypi.com>
# Copyright (c) 2018-2026, Raspberry Pi Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions, and the following disclaimer,
#    without modification.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The names of the above-listed copyright holders may not be used
#    to endorse or promote products derived from this software without
#    specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Python port of the Perl ovmerge tool. CLI wrapper - the DT tokeniser,
# parser, and merge/apply engine live in ovmerge_engine.py.

import argparse
import re
import sys

from ovmerge_engine import (
    S, OvMergeError, dtparse, dtdump, open_source_lines,
    get_prop_string, get_child, get_prop, set_prop, add_label, resolve_label,
    dtparam, get_node, ovapply1, get_fragments, delete_node, delete_prop,
    adj_ref, get_labels, ovstrip, renumber_fragments, ovmerge, ovapply2,
    is_node_empty, add_node, PROPS,
)


# Main
# ---------------------------------------------------------------------------

class RedoAction(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        cmdline = getattr(namespace, 'cmdline', [])
        cmdline.append(option_string)
        cmdline.extend(values if isinstance(values, list) else [values])
        setattr(namespace, 'cmdline', cmdline)
        setattr(namespace, self.dest, True if self.nargs == 0 else values)


def run(argv):
    parser = argparse.ArgumentParser(
        description="Merge DT overlay source files, flatten and sort .dts files, "
                    "or display the include tree.",
        argument_default=argparse.SUPPRESS,
    )
    parser.add_argument('-b', dest='branch', action=RedoAction, metavar='branch',
                        help="Read files from specified git branch")
    parser.add_argument('-B', dest='bkpt', type=int, metavar='token',
                        help="Set a debug breakpoint on the specified token number")
    parser.add_argument('-c', dest='comment', action='store_true',
                        help="Include 'redo' comment with command line (c.f. '-r')")
    parser.add_argument('-e', dest='expand', action='store_true',
                        help="Expand mode - list non-skipped lines in order of inclusion")
    parser.add_argument('-f', dest='force', action='store_true',
                        help="Force some errors to be ignored")
    parser.add_argument('-i', dest='show_includes', action='store_true',
                        help="Show include hierarchy for each file")
    parser.add_argument('-l', dest='expand_label', action='store_true',
                        help="Like expand mode, but labels each line with source file")
    parser.add_argument('-n', dest='no_dts', action='store_true',
                        help="No .dts file header (just parsing .dtsi files)")
    parser.add_argument('-N', dest='no_renumber', action='store_true',
                        help="Don't renumber overlay fragments (not guaranteed to work)")
    parser.add_argument('-p', dest='pi_extras', action=RedoAction, nargs=0,
                        help="Emulate Pi firmware manipulation")
    parser.add_argument('-q', dest='query', action='store_true',
                        help="Query mode (no output, just the success/failure return code)")
    parser.add_argument('-r', dest='redo', action='store_true',
                        help="Redo command comment in named files (c.f. '-c')")
    parser.add_argument('-s', dest='sort', action=RedoAction, nargs=0,
                        help="Sort nodes and properties (for easy comparison)")
    parser.add_argument('-S', dest='indent_spaces', action=RedoAction, metavar='n',
                        help="Instead of tabs, use 'n' spaces for indentation")
    parser.add_argument('-t', dest='trace_tree', action='store_true',
                        help="Trace the tree changes")
    parser.add_argument('-T', dest='trace_parse', action='store_true',
                        help="Trace the parsing process")
    parser.add_argument('-w', dest='warnings', action='store_true',
                        help="Show warnings")
    parser.add_argument('overlays', nargs='*', metavar='ovspec',
                        help="The name of an overlay, optionally followed by a comma-separated "
                             "list of parameters, each with optional '=<value>' assignments. The "
                             "presence of any parameters, or a comma followed by no parameters, "
                             "removes the parameter declarations from the merged overlay to avoid "
                             "a potential name clash.")

    cmdline = []
    redo_comments = []
    while True:
        options = vars(parser.parse_args(argv))
        cmdline.extend(options.pop('cmdline', []))
        args = options.pop('overlays', [])
        redo = options.pop('redo', False)
        indent_spaces = options.pop('indent_spaces', None)
        if indent_spaces is not None:
            if not re.fullmatch(r'\d+', indent_spaces):
                parser.error("argument -S: expected a non-negative integer")
            indent_spaces = int(indent_spaces)
            S.indent_str = ' ' * indent_spaces
        for name, value in options.items():
            setattr(S, name, value)
        if S.expand_label:
            S.expand = True

        if redo:
            if args:
                lines = open_source_lines(args[0])
            else:
                lines = sys.stdin.readlines()
            it = iter(lines)
            firstline = next(it, '')
            m = re.match(r'^// redo: ovmerge (.*)', firstline)
            if not m:
                parser.error("Redo but input has no 'redo:' comment")
            argv = re.split(r'\s+', m.group(1).strip())
            for line in it:
                line = line.rstrip('\n')
                if re.match(r'^/dts-v1/', line):
                    break
                redo_comments.append(line)
        else:
            break

    if not args:
        parser.error("At least one ovspec is required")

    cmdline += args

    overlays = []

    for overlay in args:
        if S.trace_tree:
            print(f"[ overlay {overlay} ]")

        m = re.match(r'^([^,:]+)', overlay)
        ovname = m.group(1) if m else ''
        overlay_rest = overlay[len(ovname):]
        dt = dtparse(ovname, S.no_dts)
        apply_params = overlay_rest.startswith(',')

        if S.show_includes or S.expand:
            continue

        model = get_prop_string(dt.root, 'model')

        S.cur_dt = dt

        if model and re.match(r'^Raspberry Pi', model) and S.pi_extras:
            aliases = get_child(dt.root, 'aliases')
            i2c = get_prop(aliases, 'i2c1')[1]
            set_prop(aliases, 'i2c', i2c)
            set_prop(aliases, 'i2c_arm', i2c)

            i2c_node = resolve_label(dt, i2c[1])
            add_label(dt, i2c_node, 'i2c_arm')

            i2c = get_prop(aliases, 'i2c0')[1]
            set_prop(aliases, 'i2c_vc', i2c)

            i2c_node = resolve_label(dt, i2c[1])
            add_label(dt, i2c_node, 'i2c_vc')

            overrides = get_child(dt.root, '__overrides__')
            i2c_prop = get_prop(overrides, 'i2c1')
            prop_rest = i2c_prop[1:]
            set_prop(overrides, 'i2c', *prop_rest)
            set_prop(overrides, 'i2c_arm', *prop_rest)

            i2c_prop = get_prop(overrides, 'i2c0')
            prop_rest = i2c_prop[1:]
            set_prop(overrides, 'i2c_vc', *prop_rest)

        if apply_params:
            seg_re = re.compile(r'[,:]([^=,]+)(?:=([^,]+))?')
            pos = 0
            while True:
                mm = seg_re.match(overlay_rest, pos)
                if not mm:
                    break
                pos = mm.end()
                dtparam(dt, mm.group(1), mm.group(2) if mm.group(2) is not None else '')
            if dt.plugin:
                ovapply1(dt)

        exports = get_node(dt, '/__exports__')
        if exports:
            for symbol in exports[PROPS]:
                expname = symbol[0]
                adj_ref(1, expname)

        if apply_params:
            delete_node(get_node(dt, '/__overrides__'))
            for fragment in get_fragments(dt):
                if get_child(fragment, '__dormant__'):
                    delete_node(fragment)

                payload = get_child(fragment, '__overlay__')
                if payload is not None and get_prop(payload, 'dtoverlay,preserve-phandle'):
                    for label in get_labels(payload):
                        adj_ref(1, label)
                    delete_prop(payload, 'dtoverlay,preserve-phandle')

        S.cur_dt = None

        if dt.plugin and not S.no_renumber:
            ovstrip(dt)

        overlays.append(dt)

    if not overlays:
        sys.exit(0)

    if overlays[0].plugin:
        if not S.no_renumber:
            renumber_fragments(overlays[0], 0)

        for i in range(1, len(overlays)):
            ovmerge(overlays[0], overlays[i])
    else:
        base = overlays[0]

        if len(overlays) > 1:
            symbols = get_child(base.root, '__symbols__')
            if symbols is None:
                symbols = add_node(base.root, '__symbols__')

            renumber_fragments(overlays[1], 0)

            for i in range(2, len(overlays)):
                ovmerge(overlays[1], overlays[i])

            ovapply2(base, overlays[1])

            if is_node_empty(symbols):
                delete_node(symbols)

    out = sys.stdout

    if S.comment:
        parts = ['// redo: ovmerge -c']
        for opt in cmdline:
            if re.search(r'\s', opt):
                parts.append(f" '{opt}'")
            else:
                parts.append(f" {opt}")
        out.write(''.join(parts) + "\n")
        if redo_comments:
            out.write("\n".join(redo_comments))
        out.write("\n")

    if not S.query:
        dtdump(overlays[0], out)

    sys.exit(S.retcode)


def main():
    try:
        run(sys.argv[1:])
    except OvMergeError as e:
        print(str(e), file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()
