# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.
"""The class of Space used to optimize the Meta parameters"""

import json
import random
from copy import deepcopy
from typing import Any

import numpy as np  # type: ignore

from tvm.s_tir import Schedule
from tvm.s_tir import meta_schedule as ms
from tvm.s_tir.meta_schedule.database import TuningRecord, Workload
from tvm.s_tir.meta_schedule.utils import remove_build_dir
from tvm.target import Target

from .utils import write_file


class Space:
    """Space class

    Parameters
    ----------
    data: json data
        A json file template
    workload: json data
        A json file workload
    target: Target data
        Target device information
    """

    def __init__(self, data: Any, workload: Any, target: Target):
        self.cfg = deepcopy(data)
        self._id = data[0]
        self.workload = Workload.from_json(workload)
        self.target = target
        self.dev = self.get_device_type(target)
        self.total_dims = 0
        self.dims: list[int] = []
        self.start: list[int] = []
        self.config_space: dict[str, list[int]] = dict()
        self.create_space()

    def __repr__(self) -> str:
        """Print the config space"""
        out = ""
        for key in self.config_space:
            out += f"{key}: dims={self.config_space[key]}\n"
        out += f"Total dimensions: {self.total_dims}\n"
        return out

    def __str__(self) -> str:
        """Print the config space"""
        out = ""
        for key in self.config_space:
            out += f"{key}: dims={self.config_space[key]}\n"
        out += f"Total dimensions: {self.total_dims}\n"
        return out

    def get_value(self, key, pos):
        """Return the space"""
        return self.config_space[key][pos]

    def add_space(self, space_list: list, element_list: list, limit=10000) -> list[int]:
        """Return a list without repeat and with limited value"""
        new_list = element_list
        for elem in space_list:
            if elem not in new_list and elem <= limit:
                new_list.append(elem)
        return new_list

    def knob2point(self, knob):
        """Convert a array to point"""
        point = 0
        for j, k in enumerate(knob):
            point += int(np.prod(self.dims[:j])) * k
        return point

    def point2knob(self, point):
        """Convert point form (single integer) to knob (vector)"""
        knob = []
        for dim in self.dims:
            knob.append(point % dim)
            point //= dim
        return knob

    def power_of_two(self, min_value: int, max_value: int) -> list:
        """Return power of two array in interval"""
        return [1 << i for i in range(min_value, max_value + 1)]

    def get_index(self, array: list, value: int):
        """returns an index if it finds the value"""
        for i in range(len(array)):
            if array[i][0] == value:
                return i
        return -1

    def template(self, values=None, create=True):
        """Generate the template from the values"""
        idx = -1
        config = deepcopy(self.cfg[1])
        for counter, cfg in enumerate(config[0][0]):
            opt = cfg[0]
            if opt == "Annotate":
                ann_key = cfg[2]
                if ann_key == ["meta_schedule.parallel"]:
                    interval = self.power_of_two(5, 9)
                elif ann_key == ["meta_schedule.vectorize"]:
                    interval = self.power_of_two(4, 8)
                elif ann_key == ["pragma_auto_unroll_max_step"]:
                    interval = self.power_of_two(7, 11)
                elif ann_key == ["meta_schedule.thread_extent_low_inclusive"]:
                    interval = self.power_of_two(5, 6)
                elif ann_key == ["meta_schedule.thread_extent_high_inclusive"]:
                    interval = self.power_of_two(8, 12)
                else:
                    continue
                idx += 1
                key = f"ann_{idx}"
                ann_value = cfg[1][1]
                if create:
                    self.config_space[key] = self.add_space(interval, [ann_value])
                else:
                    cfg[1][1] = self.get_value(key, values[idx])
            elif opt == "SamplePerfectTile":
                tile = config[0][1]
                tile_idx = self.get_index(tile, counter)
                tile_val = tile[tile_idx][1]
                interval = self.power_of_two(1, 6)
                for i in range(len(tile_val)):
                    idx += 1
                    key = f"sp_{counter}_{idx}"
                    split = tile_val[i]
                    if create:
                        self.config_space[key] = self.add_space(interval, [split])
                    else:
                        config[0][1][tile_idx][1][i] = self.get_value(key, values[idx])
            elif opt == "TransformLayout":
                del config[0][0][counter]
        if create:
            return None
        return config

    def create_space(self):
        """Create the space using Meta's space"""
        self.template(create=True)
        # print(self.config_space)
        self.dims = []
        for key in self.config_space:
            self.dims.append(len(self.config_space[key]))
        self.total_dims = 1
        if len(self.dims) > 0:
            for dim in self.dims:
                self.total_dims *= dim

    def get_device_type(self, target: Target) -> str:
        """Get the device type string from a target.

        Parameters
        ----------
        target : Target
            The target to get the device type from.

        Returns
        -------
        device_type : str
            The device type string.
        """
        if target.kind.name == "llvm":
            return "cpu"
        elif target.kind.name == "cuda":
            return "cuda"
        else:
            raise RuntimeError(f"Unsupported target kind for device type: {target.kind.name}")

    def save_log(
        self,
        path: str,
        record: ms.database.TuningRecord,
        results: ms.runner.RunnerResult,
    ) -> None:
        """Save the log file"""
        new_json = [self._id, record.as_json()]
        new_json[1][1] = results
        write_file([new_json], path, "a")

    def run(
        self,
        json_file_list,
        final_log,
        timeout=10,
        number=2,
        repeat=3,
        min_repeat_ms=0,
        cpu_cache=False,
    ):
        """Execute a log file and save"""

        builder = ms.builder.LocalBuilder(timeout_sec=timeout)
        runner = ms.runner.LocalRunner(
            evaluator_config=ms.runner.EvaluatorConfig(
                number=number,
                repeat=repeat,
                min_repeat_ms=min_repeat_ms,
                enable_cpu_cache_flush=cpu_cache,
            ),
        )

        results = np.full(len(json_file_list), [10000], dtype=list)
        records, mods = [], []
        for i, cfg in enumerate(json_file_list):
            try:
                record = TuningRecord.from_json(json.loads(json.dumps(cfg)), self.workload)
                sch = Schedule(self.workload.mod)
                # In some layers this is a heavy impact in time cost, so
                # I applied this only 25% of the samples.
                remove_postproc = random.random() > 0.75
                record.trace.apply_to_schedule(sch, remove_postproc=remove_postproc)
                mods.append(sch.mod)
                records.append(record)
            except Exception:  # pylint: disable=broad-except, invalid-name
                continue

        builder_res = builder.build([ms.builder.BuilderInput(mod, self.target) for mod in mods])

        for i, record in enumerate(records):
            try:
                inp = ms.runner.RunnerInput(
                    builder_res[i].artifact_path,
                    device_type=self.dev,
                    args_info=ms.arg_info.TensorInfo.from_prim_func(mods[i]["main"]),
                )
                runner_res = runner.run([inp])[0].result()
                results[i] = [v.value for v in runner_res.run_secs]  # type: ignore
            except Exception:  # pylint: disable=broad-except, invalid-name
                results[i] = [1e10]
                continue

            # save the solution in json file
            self.save_log(final_log, record, results[i])

            # clean up
            remove_build_dir(builder_res[i].artifact_path)
        return results
