# # ⚠ Warning
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# [🥭 Entropy Markets](https://entropy.trade/) support is available at:
#   [Docs](https://docs.entropy.trade/)
#   [Discord](https://discord.gg/67jySBhxrg)
#   [Twitter](https://twitter.com/entropymarkets)
#   [Github](https://github.com/blockworks-foundation)
#   [Email](mailto:hello@blockworks.foundation)

import base64
import construct
import hashlib
import json
import os.path
import typing

from .constants import DATA_PATH
from .layouts import layouts

_known_idl_type_adapters: typing.Dict[str, typing.Callable[[], typing.Any]] = {
    "publicKey": lambda: layouts.PublicKeyAdapter(),
    "bool": lambda: construct.Flag,
    "u8": lambda: layouts.DecimalAdapter(1),
    "u64": lambda: layouts.DecimalAdapter(),
    "u128": lambda: layouts.DecimalAdapter(16),
    "i8": lambda: layouts.SignedDecimalAdapter(1),
    "i64": lambda: layouts.SignedDecimalAdapter(),
    "i128": lambda: layouts.SignedDecimalAdapter(16),
}


class IdlType:
    def __init__(self, name: str, struct: typing.Any) -> None:
        self.name: str = name
        self.struct: typing.Any = struct


# This really only parses a subset of the IDL. For Entropy right now, we only need to parse the
# events, and they have a limited set of types.
#
# We are able to re-use the adapters from the layouts, which is great!
#
# One wrinkle is that IDL has a 'vec' type to describe its arrays. It looks to be a 4-byte prefix
# contains the length of the array, so we can't use fixed structs - we need to use the construct
# context to parse the array length, then refer to that in the next element as the length of
# the construct.Array.
def _load_idl_parsers_from_json_file(filepath: str) -> typing.Dict[bytes, IdlType]:
    def _discriminator_from_name(name: str) -> bytes:
        sha = hashlib.sha256(f"event:{name}".encode())
        return sha.digest()[0:8]

    def _context_counter_lookup(
        field_counter: str,
    ) -> typing.Callable[[typing.Any], int]:
        return lambda ctx: int(ctx[field_counter])

    with open(filepath, encoding="utf-8") as json_file:
        idl_data: typing.Dict[str, typing.Any] = json.load(json_file)

    layout_loaders: typing.Dict[bytes, IdlType] = {}
    for event in idl_data["events"]:
        event_name: str = event["name"]
        discriminator = _discriminator_from_name(event_name)
        fields = []
        for field in event["fields"]:
            field_name: str = field["name"]
            field_type: str = field["type"]
            if isinstance(field_type, dict):
                inner_type: str = field["type"]["vec"]
                counter_name: str = f"{field_name}_count"
                fields += [counter_name / construct.BytesInteger(4, swapped=True)]
                inner_loader = _known_idl_type_adapters[inner_type]
                fields += [
                    field_name
                    / construct.Array(
                        _context_counter_lookup(counter_name), inner_loader()
                    )
                ]
            else:
                fields += [field_name / _known_idl_type_adapters[field_type]()]
        layout_loaders[discriminator] = IdlType(event_name, construct.Struct(*fields))
    return layout_loaders


class IdlParser:
    def __init__(self, filepath: str):
        self.parsers: typing.Dict[bytes, IdlType] = _load_idl_parsers_from_json_file(
            filepath
        )

    def parse(self, binary_data: bytes) -> typing.Tuple[str, typing.Any]:
        discriminator: bytes = binary_data[0:8]
        idl_type: IdlType = self.parsers[discriminator]
        return idl_type.name, idl_type.struct.parse(binary_data[8:])

    def decode_and_parse(self, encoded: str) -> typing.Tuple[str, typing.Any]:
        decoded: bytes = base64.b64decode(encoded)
        return self.parse(decoded)


_loaded_idl_sets: typing.Dict[str, IdlParser] = {}


def lazy_load_cached_idl_parser(filename: str) -> IdlParser:
    if filename not in _loaded_idl_sets:
        filepath = os.path.join(DATA_PATH, filename)
        idl_parser: IdlParser = IdlParser(filepath)
        _loaded_idl_sets[filename] = idl_parser
    return _loaded_idl_sets[filename]
