# -*- coding: utf-8 -*-

"""
存储音·创附属子类
"""

"""
版权所有 © 2025 金羿 & 诸葛亮与八卦阵
Copyright © 2025 Eilles & bgArray

开源相关声明请见 仓库根目录下的 License.md
Terms & Conditions: License.md in the root directory
"""

# 睿乐组织 开发交流群 861684859
# Email TriM-Organization@hotmail.com
# 若需转载或借鉴 许可声明请查看仓库目录下的 License.md

from math import sin, cos, asin, radians, degrees, sqrt, atan
from dataclasses import dataclass
from typing import Optional, Any, List, Tuple, Union, Dict, Sequence

from Musicreater.constants import MC_PITCHED_INSTRUMENT_LIST
from Musicreater.builtin_plugins.to_commands.main import MineCommand
from Musicreater.builtin_plugins.to_commands.progressbar import (
    ProgressBarStyle,
    mctick2timestr,
    DEFAULT_PROGRESSBAR_STYLE,
)


@dataclass(init=False)
class MineNote:
    """存储单个音符的类"""

    sound_name: str
    """乐器ID"""

    note_pitch: int
    """midi音高"""

    velocity: int
    """力度"""

    start_tick: int
    """开始之时 命令刻"""

    duration: int
    """音符持续时间 命令刻"""

    high_precision_time: int
    """高精度开始时间偏量 1/1250 秒"""

    percussive: bool
    """是否作为打击乐器启用"""

    sound_distance: float
    """声源距离 方块"""

    sound_azimuth: Tuple[float, float]
    """声源方位 角度"""

    extra_info: Dict[str, Any]
    """你觉得放什么好？"""

    def __init__(
        self,
        mc_sound_name: str,
        midi_pitch: Optional[int],
        midi_velocity: int,
        start_time: int,
        last_time: int,
        mass_precision_time: int = 0,
        is_percussion: Optional[bool] = None,
        distance: Optional[float] = None,
        azimuth: Optional[Tuple[float, float]] = None,
        extra_information: Dict[str, Any] = {},
    ):
        """
        用于存储单个音符的类

        Parameters
        ------------
        mc_sound_name: str
            《我的世界》声音ID
        midi_pitch: int
            midi音高
        midi_velocity: int
            midi响度(力度)
        start_time: int
            开始之时(命令刻)
            注：此处的时间是用从乐曲开始到当前的刻数
        last_time: int
            音符延续时间(命令刻)
        mass_precision_time: int
            高精度的开始时间偏移量(1/1250秒)
        is_percussion: bool
            是否作为打击乐器
        distance: float
            发声源距离玩家的距离（半径 `r`）
            注：距离越近，音量越高，默认为 0。此参数可以与音量成某种函数关系。
        azimuth: tuple[float, float]
            声源方位
            注：此参数为tuple，包含两个元素，分别表示：
            `rV`  发声源在竖直（上下）轴上，从玩家视角正前方开始，向顺时针旋转的角度
            `rH`  发声源在水平（左右）轴上，从玩家视角正前方开始，向上（到达玩家正上方顶点后变为向下，以此类推的旋转）旋转的角度
        extra_information: Dict[str, Any]
            附加信息，尽量存储为字典

        Returns
        ---------
        MineNote 类
        """
        self.sound_name: str = mc_sound_name
        """乐器ID"""
        self.note_pitch: int = 66 if midi_pitch is None else midi_pitch
        """midi音高"""
        self.velocity: int = midi_velocity
        """响度(力度)"""
        self.start_tick: int = start_time
        """开始之时 命令刻"""
        self.duration: int = last_time
        """音符持续时间 命令刻"""
        self.high_precision_time: int = mass_precision_time
        """高精度开始时间偏量 0.4 毫秒"""

        self.percussive = (
            (mc_sound_name not in MC_PITCHED_INSTRUMENT_LIST)
            if (is_percussion is None)
            else is_percussion
        )
        """是否为打击乐器"""

        self.sound_azimuth = (azimuth[0] % 360, azimuth[1] % 360) if azimuth else (0, 0)
        """声源方位"""

        # 如果指定为零，那么为零，但如果不指定或者指定为负数，则为 0.01 的距离
        self.sound_distance = (
            (16 if distance > 16 else (distance if distance >= 0 else 0.01))
            if distance is not None
            else 0.01
        )
        """声源距离"""

        self.extra_info = extra_information if extra_information else {}

    @classmethod
    def from_traditional(
        cls,
        mc_sound_name: str,
        midi_pitch: Optional[int],
        midi_velocity: int,
        start_time: int,
        last_time: int,
        mass_precision_time: int = 0,
        is_percussion: Optional[bool] = None,
        displacement: Optional[Tuple[float, float, float]] = None,
        extra_information: Optional[Any] = None,
    ):
        """
        从传统音像位移格式传参，写入用于存储单个音符的类

        Parameters
        ------------
        mc_sound_name: str
            《我的世界》声音ID
        midi_pitch: int
            midi音高
        midi_velocity: int
            midi响度(力度)
        start_time: int
            开始之时(命令刻)
            注：此处的时间是用从乐曲开始到当前的刻数
        last_time: int
            音符延续时间(命令刻)
        mass_precision_time: int
            高精度的开始时间偏移量(1/1250秒)
        is_percussion: bool
            是否作为打击乐器
        displacement: tuple[float, float, float]
            声像位移
        extra_information: Any
            附加信息，尽量为字典。

        Returns
        ---------
        MineNote 类
        """

        if displacement is None:
            displacement = (0, 0, 0)
            r = 0
            alpha_v = 0
            beta_h = 0
        else:
            r = sqrt(displacement[0] ** 2 + displacement[1] ** 2 + displacement[2] ** 2)
            if r == 0:
                alpha_v = 0
                beta_h = 0
            else:
                beta_h = round(degrees(asin(displacement[1] / r)), 8)
                if displacement[2] == 0:
                    alpha_v = -90 if displacement[0] > 0 else 90
                else:
                    alpha_v = round(
                        degrees(atan(-displacement[0] / displacement[2])), 8
                    )

        return cls(
            mc_sound_name=mc_sound_name,
            midi_pitch=midi_pitch,
            midi_velocity=midi_velocity,
            start_time=start_time,
            last_time=last_time,
            mass_precision_time=mass_precision_time,
            is_percussion=is_percussion,
            distance=r,
            azimuth=(alpha_v, beta_h),
            extra_information=(
                (
                    extra_information
                    if isinstance(extra_information, dict)
                    else {"EXTRA_INFO": extra_information}
                )
                if extra_information
                else {}
            ),
        )

    @property
    def position_displacement(self) -> Tuple[float, float, float]:
        """声像位移"""
        dk1 = self.sound_distance * round(cos(radians(self.sound_azimuth[1])), 8)
        return (
            -dk1 * round(sin(radians(self.sound_azimuth[0])), 8),
            self.sound_distance * round(sin(radians(self.sound_azimuth[1])), 8),
            dk1 * round(cos(radians(self.sound_azimuth[0])), 8),
        )

    @classmethod
    def decode(cls, code_buffer: bytes, is_high_time_precision: bool = True):
        """自字节码析出 MineNote 类"""
        group_1 = int.from_bytes(code_buffer[:6], "big")
        percussive_ = bool(group_1 & 0b1)
        duration_ = (group_1 := group_1 >> 1) & 0b11111111111111111
        start_tick_ = (group_1 := group_1 >> 17) & 0b11111111111111111
        note_pitch_ = (group_1 := group_1 >> 17) & 0b1111111
        sound_name_length = group_1 >> 7

        if code_buffer[6] & 0b1:
            distance_ = (
                code_buffer[8 + sound_name_length]
                if is_high_time_precision
                else code_buffer[7 + sound_name_length]
            ) / 15

            group_2 = int.from_bytes(
                (
                    code_buffer[9 + sound_name_length : 14 + sound_name_length]
                    if is_high_time_precision
                    else code_buffer[8 + sound_name_length : 13 + sound_name_length]
                ),
                "big",
            )
            azimuth_ = ((group_2 >> 20) / 2912, (group_2 & 0xFFFFF) / 2912)

        else:
            distance_ = 0
            azimuth_ = (0, 0)

        try:
            return cls(
                mc_sound_name=(
                    o := (
                        code_buffer[8 : 8 + sound_name_length]
                        if is_high_time_precision
                        else code_buffer[7 : 7 + sound_name_length]
                    )
                ).decode(encoding="GB18030"),
                midi_pitch=note_pitch_,
                midi_velocity=code_buffer[6] >> 1,
                start_time=start_tick_,
                last_time=duration_,
                mass_precision_time=code_buffer[7] if is_high_time_precision else 0,
                is_percussion=percussive_,
                distance=distance_,
                azimuth=azimuth_,
            )
        except:
            print(code_buffer, "\n", o)
            raise

    def encode(
        self, is_displacement_included: bool = True, is_high_time_precision: bool = True
    ) -> bytes:
        """
        将数据打包为字节码

        Parameters
        ------------
        is_displacement_included: bool
            是否包含声像偏移数据，默认为**是**
        is_high_time_precision: bool
            是否启用高精度，默认为**是**

        Returns
        ---------
        bytes
            打包好的字节码
        """

        # MineNote 的字节码共有三个顺次版本分别如下

        # 字符串长度 6 位 支持到 63
        # note_pitch 7 位 支持到 127
        # start_tick 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
        # duration 17 位 支持到 131071 即 109.22583 分钟 合 1.8204305 小时
        # percussive 长度 1 位 支持到 1
        # 共 48 位 合 6 字节
        # +++
        # velocity 长度 7 位 支持到 127
        # is_displacement_included 长度 1 位 支持到 1
        # 共 8 位 合 1 字节
        # +++
        # （在第二版中已舍弃）
        # track_no 长度 8 位 支持到 255 合 1 字节
        # （在第二版中新增）
        # high_time_precision（可选）长度 8 位 支持到 255 合 1 字节 支持 1/1250 秒
        # +++
        # sound_name 长度最多 63 支持到 31 个中文字符 或 63 个西文字符
        # 第一版编码： UTF-8
        # 第二版编码： GB18030
        # +++
        # （在第三版中已废弃）
        # position_displacement 每个元素长 16 位 合 2 字节
        # 共 48 位 合 6 字节 支持存储三位小数和两位整数，其值必须在 [0, 65.535] 之间
        # （在第三版中新增）
        # sound_distance 8 位 支持到 255 即 16 格 合 1 字节（按值放大 15 倍存储，精度可达 1 / 15）
        # sound_azimuth 每个元素长 20 位 共 40 位 合 5 字节。每个值放大 2912 倍存储，即支持到 360.08756868131866 度，精度同理

        return (
            (
                (
                    (
                        (
                            (
                                (
                                    (
                                        (
                                            len(
                                                r := self.sound_name.encode(
                                                    encoding="GB18030"
                                                )
                                            )
                                            << 7
                                        )
                                        + self.note_pitch
                                    )
                                    << 17
                                )
                                + self.start_tick
                            )
                            << 17
                        )
                        + self.duration
                    )
                    << 1
                )
                + self.percussive
            ).to_bytes(6, "big")
            + ((self.velocity << 1) + is_displacement_included).to_bytes(1, "big")
            # + self.track_no.to_bytes(1, "big")
            + (
                self.high_precision_time.to_bytes(1, "big")
                if is_high_time_precision
                else b""
            )
            + r
            + (
                (
                    round(self.sound_distance * 15).to_bytes(1, "big")
                    + (
                        (round(self.sound_azimuth[0] * 2912) << 20)
                        + round(self.sound_azimuth[1] * 2912)
                    ).to_bytes(5, "big")
                )
                if is_displacement_included
                else b""
            )
        )

    def set_info(self, key: Union[str, Sequence[str]], value: Any):
        """设置附加信息"""
        if isinstance(key, str):
            self.extra_info[key] = value
        elif (
            isinstance(key, Sequence)
            and isinstance(value, Sequence)
            and (k := len(key)) == len(value)
        ):
            for i in range(k):
                self.extra_info[key[i]] = value[i]
        else:
            # 提供简单报错就行了，如果放一堆 if 语句，降低处理速度
            raise TypeError("参数类型错误；键：`{}` 值：`{}`".format(key, value))

    def get_info(self, key: str) -> Any:
        """获取附加信息"""
        if key in self.extra_info:
            return self.extra_info[key]
        elif "EXTRA_INFO" in self.extra_info:
            if (
                isinstance(self.extra_info["EXTRA_INFO"], dict)
                and key in self.extra_info["EXTRA_INFO"]
            ):
                return self.extra_info["EXTRA_INFO"].get(key)
            else:
                return self.extra_info["EXTRA_INFO"]
        else:
            return None

    def stringize(
        self, include_displacement: bool = False, include_extra_data: bool = False
    ) -> str:
        return (
            "{}Note(Instrument = {}, {}Velocity = {}, StartTick = {}, Duration = {}{}".format(
                "Percussive" if self.percussive else "",
                self.sound_name,
                "" if self.percussive else "NotePitch = {}, ".format(self.note_pitch),
                self.velocity,
                self.start_tick,
                self.duration,
            )
            + (
                ", SoundDistance = `r`{}, SoundAzimuth = (`αV`{}, `βH`{})".format(
                    self.sound_distance, *self.sound_azimuth
                )
                if include_displacement
                else ""
            )
            + (", ExtraData = {}".format(self.extra_info) if include_extra_data else "")
            + ")"
        )

    def tuplize(self, is_displacement: bool = False):
        tuplized = self.__tuple__()
        return tuplized[:-2] + (tuplized[-2:] if is_displacement else ())

    def __list__(self) -> List:
        return (
            [
                self.percussive,
                self.sound_name,
                self.velocity,
                self.start_tick,
                self.duration,
                self.sound_distance,
                self.sound_azimuth,
            ]
            if self.percussive
            else [
                self.percussive,
                self.sound_name,
                self.note_pitch,
                self.velocity,
                self.start_tick,
                self.duration,
                self.sound_distance,
                self.sound_azimuth,
            ]
        )

    def __tuple__(
        self,
    ) -> Union[
        Tuple[bool, str, int, int, int, int, float, Tuple[float, float]],
        Tuple[bool, str, int, int, int, float, Tuple[float, float]],
    ]:
        return (
            (
                self.percussive,
                self.sound_name,
                self.velocity,
                self.start_tick,
                self.duration,
                self.sound_distance,
                self.sound_azimuth,
            )
            if self.percussive
            else (
                self.percussive,
                self.sound_name,
                self.note_pitch,
                self.velocity,
                self.start_tick,
                self.duration,
                self.sound_distance,
                self.sound_azimuth,
            )
        )

    def __dict__(self):
        return (
            {
                "Percussive": self.percussive,
                "Instrument": self.sound_name,
                "Velocity": self.velocity,
                "StartTick": self.start_tick,
                "Duration": self.duration,
                "SoundDistance": self.sound_distance,
                "SoundAzimuth": self.sound_azimuth,
                "ExtraData": self.extra_info,
            }
            if self.percussive
            else {
                "Percussive": self.percussive,
                "Instrument": self.sound_name,
                "Pitch": self.note_pitch,
                "Velocity": self.velocity,
                "StartTick": self.start_tick,
                "Duration": self.duration,
                "SoundDistance": self.sound_distance,
                "SoundAzimuth": self.sound_azimuth,
                "ExtraData": self.extra_info,
            }
        )

    def __eq__(self, other) -> bool:
        if not isinstance(other, self.__class__):
            return False
        return self.tuplize() == other.tuplize()


@dataclass(init=False)
class SingleNoteBox:
    """存储单个音符盒"""

    instrument_block: str
    """乐器方块"""

    note_value: int
    """音符盒音高"""

    annotation_text: str
    """音符注释"""

    is_percussion: bool
    """是否为打击乐器"""

    def __init__(
        self,
        instrument_block_: str,
        note_value_: int,
        percussion: Optional[bool] = None,
        annotation: str = "",
    ):
        """
        用于存储单个音符盒的类

        Parameters
        ------------
        instrument_block_: str
            音符盒演奏所使用的乐器方块
        note_value_: int
            音符盒的演奏音高
        percussion: bool
            此音符盒乐器是否作为打击乐处理
            注：若为空，则自动识别是否为打击乐器
        annotation: Any
            音符注释

        Returns
        ---------
        SingleNoteBox 类
        """

        self.instrument_block = instrument_block_
        """乐器方块"""
        self.note_value = note_value_
        """音符盒音高"""
        self.annotation_text = annotation
        """音符注释"""
        if percussion is None:
            self.is_percussion = percussion not in MC_PITCHED_INSTRUMENT_LIST
        else:
            self.is_percussion = percussion

    @property
    def inst(self) -> str:
        """获取音符盒下的乐器方块"""
        return self.instrument_block

    @inst.setter
    def inst(self, inst_):
        self.instrument_block = inst_

    @property
    def note(self) -> int:
        """获取音符盒音调特殊值"""
        return self.note_value

    @note.setter
    def note(self, note_):
        self.note_value = note_

    @property
    def annotation(self) -> str:
        """获取音符盒的备注"""
        return self.annotation_text

    @annotation.setter
    def annotation(self, annotation_):
        self.annotation_text = annotation_

    def copy(self):
        return SingleNoteBox(
            instrument_block_=self.instrument_block,
            note_value_=self.note_value,
            annotation=self.annotation_text,
        )

    def __str__(self) -> str:
        return f"Note(inst = {self.inst}, note = {self.note}, )"

    def __tuple__(self) -> tuple:
        return self.inst, self.note, self.annotation

    def __dict__(self) -> dict:
        return {
            "inst": self.inst,
            "note": self.note,
            "annotation": self.annotation,
        }

    def __eq__(self, other) -> bool:
        if not isinstance(other, self.__class__):
            return False
        return self.__str__() == other.__str__()
