#!/usr/bin/env python3
# Copyright © 2025 Intel Corporation
# SPDX-License-Identifier: MIT

import argparse
import copy
import ctypes
import enum
import fcntl
import json
import pathlib
import os
import re
import sys

from gen_opcodes import OPCODES, Props


def c_bool(value):
    return 'true' if value else 'false'


def opcode_has_dst(opcode):
    return not bool(opcode.props & Props.NO_DST)


H_TEMPLATE = """\
/*
 * Copyright © 2026 Intel Corporation
 * SPDX-License-Identifier: MIT
 *
 * Autogenerated file, do not edit!
 */

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus

<%def name="comma_join(l)">${', '.join([str(i) for i in l])}</%def>

struct gen_encoding_${info.name.lower()} {
   static constexpr gen_encoding_type TYPE = GEN_ENCODING_${info.name.upper()};

   /* Instruction fields */
% for (i, rs) in info.fields.items():
   static constexpr auto ${i} = gen_ranges<${len(rs)}>({
% for r in rs:
      gen_range { ${comma_join(r)} },
% endfor
   });
% endfor

   /* Compact instruction fields */
% for (i, rs) in info.compact_fields.items():
   static constexpr auto C_${i} = gen_ranges<${len(rs)}>({
% for r in rs:
      gen_range { ${comma_join(r)} },
% endfor
   });
% endfor

   /* Instruction sub fields */
% for (i, rs) in info.sub_fields.items():
   static constexpr auto ${i} = gen_sub_ranges<${len(rs)}>({
% for r in rs:
      gen_sub_range { ${comma_join(r)} },
% endfor
   });
% endfor

   static constexpr std::array<gen_inst_description, 128> gen_to_description =
   []() constexpr {
      std::array<gen_inst_description, 128> r;
% for (op, hw) in info.gen_op_to_hw:
      r[GEN_OP_${op.enum_name}] = gen_inst_description(GEN_OP_${op.enum_name}, ${hw}, ${op.format}, ${c_bool(opcode_has_dst(op))});
% endfor
      return r;
   }();

   static constexpr std::array<gen_inst_description, 128> hw_to_description =
   []() constexpr {
      std::array<gen_inst_description, 128> r;
% for (op, hw) in info.gen_op_to_hw:
      r[${hw}] = gen_inst_description(GEN_OP_${op.enum_name}, ${hw}, ${op.format}, ${c_bool(opcode_has_dst(op))});
% endfor
      return r;
   }();
};

#endif

#ifdef __cplusplus
}
#endif
"""


class InstructionInfo:

    def __init__(self, raw_info, name):
        self.name = name
        self.imported = set()
        self.target = self.__init_with_inheritance(raw_info, name)
        self.__process_ranges()
        assert name in self.imported

    def __init_with_inheritance(self, raw_info, name):
        assert name in raw_info
        import_name = raw_info[name].get('import')
        if import_name:
            assert import_name not in self.imported
            self.imported.add(import_name)
            base = self.__init_with_inheritance(raw_info, import_name)
        else:
            base = dict()

        def find_dicts(d):
            return set({ k for k, v in d.items() if isinstance(v, dict) })

        new_target = copy.deepcopy(raw_info[name])

        # Drop all comment items. (object key names that start with a '#')
        for d in find_dicts(new_target):
            new_target[d] = { k: v for k, v
                              in new_target[d].items()
                              if not k.startswith('#') }

        if import_name:
            needed = find_dicts(new_target) - find_dicts(base)
            base.update({ k: {} for k in needed })
            needed = find_dicts(base) - find_dicts(new_target)
            new_target.update({ k: {} for k in needed })
            # Merge imported fields, but prefer current version
            dicts = find_dicts(new_target)
            for d in dicts:
                old_only = { k: v for k, v in base[d].items()
                             if k not in new_target[d] }
                assert len(set(old_only.keys()).intersection(set(new_target[d].keys()))) == 0
                new_target[d].update(old_only)

        required_infos = ('fields', 'sub-fields', 'compact-fields')
        for info in required_infos:
            if info not in new_target:
                new_target[info] = {}

        # Drop all items that are None (null in json)
        for d in find_dicts(new_target):
            new_target[d] = { k: v for k, v in new_target[d].items()
                              if v is not None }

        self.imported.add(name)
        return new_target

    def __process_ranges(self):
        """Process ranges in self.target

        On input, the fields are lists of integers. On output, they
        are list of tuples of length 2 where the first item is the
        high bit number of the range and the second item is the low
        bit number of the range.

        Additionally, any adjacent ranges that happen to have bit
        ranges that can be combined are collapsed into a single range.

        """

        def collapse_ranges(ranges):
            """Generator that merges compatible ranges."""

            it = iter(ranges)
            r = next(it)
            for n in it:
                if (r[1] - 1) == n[0]:
                    r = (r[0], n[1])
                else:
                    yield r
                    r = n
            yield r

        def pair_up(v):
            if v is None:
                return None
            assert (len(v) % 2) == 0
            ranges = [(v[i], v[i + 1]) for i in range(0, len(v), 2)]
            return list(collapse_ranges(ranges));

        for field_type in ('fields', 'sub-fields', 'compact-fields'):
            d = self.target.get(field_type, dict())
            d = { k: pair_up(v) for k, v in d.items() }
            self.target[field_type] = d

    def __getattr__(self, name):
        dashed_name = name.replace('_', '-')
        if dashed_name in self.target:
            return self.target[dashed_name]
        raise AttributeError

    @property
    def gen_op_to_hw(self):
        encoding = self.name.lower()
        result = []
        for op in OPCODES:
            hw = op.hw_opcode[encoding]
            if hw is not None:
                result.append((op, hw))
        return result

    def for_mako(self):
        return self.target

class GenInstructionSources:

    def __init__(self, args):
        self.args = args
        self.raw_info = {}
        for in_json in self.args.json:
            in_json = pathlib.Path(in_json)
            assert in_json.is_file()
            self.outdir = pathlib.Path(args.output_dir)
            assert self.outdir.is_dir()
            with open(in_json, 'r',  encoding='utf8') as json_file:
                new_info = json.load(json_file)
                # Make sure there is no overlap in json input
                assert all({k not in self.raw_info for k in new_info.keys()})
                self.raw_info.update(new_info)

        import mako.template
        self.h_template = mako.template.Template(H_TEMPLATE)

    def gen(self):
        gen_count = 0
        if self.args.cpp:
            gen_count += self.gen_cpp_headers()
        if gen_count == 0:
            print("info: No sources specified to be generated")
        return gen_count > 0

    def gen_cpp_header(self, name):
        info = InstructionInfo(self.raw_info, name)
        template_input = {
            "info": info,
            "c_bool": c_bool,
            "opcode_has_dst": opcode_has_dst,
        }
        filename = self.outdir / f'gen_info_{name.lower()}.h'
        with open(filename, 'w', encoding='utf8') as h:
            try:
                h.write(self.h_template.render(**template_input))
            except:
                import mako.exceptions
                print(mako.exceptions.text_error_template().render())
                raise
        return True

    def gen_cpp_headers(self):
        count = 0
        names = list(self.raw_info.keys())
        for name in names:
            if self.gen_cpp_header(name):
                count += 1
        return count

class GenInstructionsApp:

    def __init__(self):
        self.parse_args()
        self.gen_sources = GenInstructionSources(self.args)
        sys.exit(0 if self.gen_sources.gen() else 1)

    def parse_args(self):
        p = argparse.ArgumentParser()
        p.add_argument("-v", "--verbose", action="count", default=0,
                       help="Enable verbose output")
        p.add_argument("--json", action="append", default=[],
                       help="Input json describing the target")
        p.add_argument("--output-dir", required=True,
                       help="Output directory for generated files")
        p.add_argument("--cpp", action="store_true", default=False,
                       help="Generate C++ headers")

        a = p.parse_args()

        self.args = a
        self.verbose = a.verbose


if __name__ == "__main__":
    GenInstructionsApp()
