import json
import logging
from typing import Dict, List, Tuple

from kloppy.domain import (
    AttackingDirection,
    BallOutEvent,
    BallState,
    BodyPart,
    BodyPartQualifier,
    CardEvent,
    CardType,
    DatasetFlag,
    Event,
    EventDataset,
    EventType,
    FoulCommittedEvent,
    GenericEvent,
    Ground,
    Metadata,
    Orientation,
    PassEvent,
    PassResult,
    Period,
    Player,
    Point,
    Provider,
    Qualifier,
    RecoveryEvent,
    Score,
    SetPieceQualifier,
    SetPieceType,
    ShotEvent,
    ShotResult,
    SubstitutionEvent,
    Team,
    Transformer,
    build_coordinate_system,
)
from kloppy.infra.serializers.event import EventDataSerializer
from kloppy.utils import Readable, performance_logging


logger = logging.getLogger(__name__)


DF_EVENT_CLASS_STATUS = "status"
DF_EVENT_CLASS_NUTMEGS = "nutmegs"
DF_EVENT_CLASS_STEALINGS = "stealings"
DF_EVENT_CLASS_FOULS = "fouls"
DF_EVENT_CLASS_THROW_IN = "throwIn"
DF_EVENT_CLASS_INCORRECT_PASSES = "incorrectPasses"
DF_EVENT_CLASS_CLEARANCES = "clearances"
DF_EVENT_CLASS_CORRECT_PASSES = "correctPasses"
DF_EVENT_CLASS_SOMBRERO_FLICK = "sombreroFlick"
DF_EVENT_CLASS_GOALKICK = "goalkick"
DF_EVENT_CLASS_GOALS = "goals"
DF_EVENT_CLASS_SHOTS = "shots"
DF_EVENT_CLASS_OFFSIDES = "offsides"
DF_EVENT_CLASS_RED_CARDS = "redCards"
DF_EVENT_CLASS_YELLOW_CARDS = "yellowCards"
DF_EVENT_CLASS_SUBSTITUTIONS = "substitutions"
DF_EVENT_CLASS_PENALTY_SHOOTOUT = "penaltyShootout"
DF_EVENT_CLASS_CORNERKICKS = "cornerKicks"

DF_EVENT_CLASS_PASSES = {
    DF_EVENT_CLASS_THROW_IN,
    DF_EVENT_CLASS_INCORRECT_PASSES,
    DF_EVENT_CLASS_CORRECT_PASSES,
    DF_EVENT_CLASS_CORNERKICKS,
}
DF_EVENT_CLASS_CARDS = {DF_EVENT_CLASS_YELLOW_CARDS, DF_EVENT_CLASS_RED_CARDS}

# status event types
DF_EVENT_TYPE_STATUS_MATCH_START = 1
DF_EVENT_TYPE_STATUS_MATCH_END = 2
DF_EVENT_TYPE_STATUS_FIRST_HALF_END = 17
DF_EVENT_TYPE_STATUS_SECOND_HALF_START = 18
DF_EVENT_TYPE_STATUS_SECOND_HALF_END = 49
DF_EVENT_TYPE_STATUS_FIRST_EXTRA_START = 50
DF_EVENT_TYPE_STATUS_FIRST_EXTRA_END = 51
DF_EVENT_TYPE_STATUS_SECOND_EXTRA_START = 52
DF_EVENT_TYPE_STATUS_SECOND_EXTRA_END = 53
DF_EVENT_TYPE_STATUS_PENALTY_SHOOTOUT_START = 54

# card event types
DF_EVENT_TYPE_YELLOW_CARD = 3
DF_EVENT_TYPE_RED_CARD_DOUBLE_YELLOW = 4
DF_EVENT_TYPE_RED_CARD = 5

# shots
DF_EVENT_TYPE_SHOT_OPEN_PLAY_GOAL = 9
DF_EVENT_TYPE_SHOT_OWN_GOAL = 10
DF_EVENT_TYPE_SHOT_HEAD_GOAL = 11
DF_EVENT_TYPE_SHOT_FREE_KICK_GOAL = 12
DF_EVENT_TYPE_SHOT_PENALTY_GOAL = 13
DF_EVENT_TYPE_SHOT_OFF_TARGET = 33
DF_EVENT_TYPE_SHOT_POST = 34
DF_EVENT_TYPE_SHOT_SAVED = 35
DF_EVENT_TYPE_SHOT_PENALTY_SAVED = 43
DF_EVENT_TYPE_SHOT_PENALTY_OFF_TARGET = 44
DF_EVENT_TYPE_SHOT_PENALTY_POST = 45

GOAL_EVENTS = {
    DF_EVENT_TYPE_SHOT_OPEN_PLAY_GOAL,
    DF_EVENT_TYPE_SHOT_OWN_GOAL,
    DF_EVENT_TYPE_SHOT_HEAD_GOAL,
    DF_EVENT_TYPE_SHOT_FREE_KICK_GOAL,
    DF_EVENT_TYPE_SHOT_PENALTY_GOAL,
}

PENALTY_SHOT_EVENTS = {
    DF_EVENT_TYPE_SHOT_PENALTY_GOAL,
    DF_EVENT_TYPE_SHOT_PENALTY_SAVED,
    DF_EVENT_TYPE_SHOT_PENALTY_OFF_TARGET,
    DF_EVENT_TYPE_SHOT_PENALTY_POST,
}

# ball out events
DF_EVENT_TYPE_CORNER_KICK = 28
DF_EVENT_TYPE_THROW_IN = 46
DF_EVENT_TYPE_GOAL_KICK = 48

BALL_OUT_EVENTS = {
    DF_EVENT_TYPE_THROW_IN,
    DF_EVENT_TYPE_CORNER_KICK,
    DF_EVENT_TYPE_GOAL_KICK,
}

# stealings
DF_EVENT_TYPE_CLEARANCE = 29
DF_EVENT_TYPE_CLEARANCE_TO_CORNER = 30
DF_EVENT_TYPE_STEALING = 182

# fouls
DF_EVENT_TYPE_FOUL_PENALTY = 31
DF_EVENT_TYPE_HAND_PENALTY = 32
DF_EVENT_TYPE_FOUL = 36
DF_EVENT_TYPE_HAND = 37
DF_EVENT_TYPE_OFFSIDE = 47

FOUL_FREE_KICK_EVENTS = {
    DF_EVENT_TYPE_FOUL,
    DF_EVENT_TYPE_HAND,
}

# passes
DF_EVENT_TYPE_PASS_CORRECT = 180
DF_EVENT_TYPE_PASS_INCORRECT = 181

# misc
DF_EVENT_TYPE_NUTMEG = 507
DF_EVENT_TYPE_SOMBRERO_FLICK = 509
DF_EVENT_TYPE_PENALTY_SHOOTOUT_GOAL = 55
DF_EVENT_TYPE_PENALTY_SHOOTOUT_SAVED = 56
DF_EVENT_TYPE_PENALTY_SHOOTOUT_OFF_TARGET = 57
DF_EVENT_TYPE_PENALTY_SHOOTOUT_POST = 183


def parse_str_ts(raw_event: Dict) -> float:
    return raw_event["t"]["m"] * 60 + (raw_event["t"]["s"] or 0)


def _parse_coordinates(coordinates: Dict[str, float]) -> Point:
    # location is cell based
    # +-------+-------+
    # | -1,-1 |  1,-1 |
    # +-------+-------+
    # | -1,1  |  1,1  |
    # +-------+-------+
    return Point(x=coordinates["x"], y=coordinates["y"])


def _get_team_and_player(
    raw_event: Dict, home_team: Team, away_team: Team
) -> Tuple[Team, Player]:
    team = None
    player = None

    team_id = raw_event.get("team")
    if team_id is not None:
        team = home_team if str(team_id) == home_team.team_id else away_team

    if "plyrId" in raw_event:
        if team is not None:
            player = team.get_player_by_id(raw_event["plyrId"])
        else:
            # NOTE: sometime events are missing team
            player = home_team.get_player_by_id(raw_event["plyrId"])
            if player is not None:
                team = home_team
            else:
                player = away_team.get_player_by_id(raw_event["plyrId"])
                team = away_team
    return team, player


def _get_event_qualifiers(
    raw_event: Dict, previous_event: Dict = None
) -> List[Qualifier]:
    qualifiers = []

    if raw_event["type"] == DF_EVENT_TYPE_THROW_IN:
        qualifiers.append(SetPieceQualifier(value=SetPieceType.THROW_IN))

    if raw_event["type"] == DF_EVENT_TYPE_CORNER_KICK:
        qualifiers.append(SetPieceQualifier(value=SetPieceType.CORNER_KICK))

    if raw_event["type"] in PENALTY_SHOT_EVENTS:
        qualifiers.append(SetPieceQualifier(value=SetPieceType.PENALTY))

    if raw_event["type"] == DF_EVENT_TYPE_SHOT_HEAD_GOAL:
        qualifiers.append(BodyPartQualifier(value=BodyPart.HEAD))

    if (
        previous_event is not None
        and previous_event["type"] == DF_EVENT_TYPE_GOAL_KICK
        and previous_event["plyrId"] == raw_event["plyrId"]
    ):
        qualifiers.append(SetPieceQualifier(value=SetPieceType.GOAL_KICK))

    if raw_event["type"] == DF_EVENT_TYPE_SHOT_FREE_KICK_GOAL or (
        previous_event is not None
        and previous_event["type"] in FOUL_FREE_KICK_EVENTS
    ):
        qualifiers.append(SetPieceQualifier(value=SetPieceType.FREE_KICK))

    if previous_event is None or previous_event["type"] in GOAL_EVENTS:
        qualifiers.append(SetPieceQualifier(value=SetPieceType.KICK_OFF))

    return qualifiers


def _parse_pass(
    raw_event: Dict,
    team: Team,
    previous_event: Dict = None,
    next_event: Dict = None,
) -> Dict:
    if next_event is not None and next_event["type"] == DF_EVENT_TYPE_OFFSIDE:
        result = PassResult.OFFSIDE
    elif raw_event["type"] == DF_EVENT_TYPE_PASS_CORRECT:
        result = PassResult.COMPLETE
    elif next_event is not None and next_event["type"] in BALL_OUT_EVENTS:
        result = PassResult.OUT
    else:
        result = PassResult.INCOMPLETE

    receiver_coordinates = None
    # NOTE: in really rare cases coordinates may be missing
    coordinates = raw_event.get("coord", {}).get("2")
    if coordinates is not None:
        receiver_coordinates = _parse_coordinates(coordinates)

    # find receiver player, if possible
    receiver_player = None
    if "recvId" in raw_event:
        receiver_player = team.get_player_by_id(raw_event["recvId"])

    elif raw_event["type"] in (
        DF_EVENT_TYPE_THROW_IN,
        DF_EVENT_TYPE_CORNER_KICK,
    ):
        # there is no receiver data in this case, use next_event to deduce receiver
        if next_event is not None:
            if (
                next_event["team"] != raw_event["team"]
                and next_event["type"] == DF_EVENT_TYPE_FOUL
                and "recvId" in next_event
            ):
                # foul from the opposite team
                receiver_player = team.get_player_by_id(next_event["recvId"])
                result = PassResult.COMPLETE
            elif (
                next_event["team"] == raw_event["team"]
                and "plyrId" in next_event
            ):
                # or same team event with plyrId
                receiver_player = team.get_player_by_id(next_event["plyrId"])
                result = PassResult.COMPLETE

    qualifiers = _get_event_qualifiers(raw_event, previous_event)

    return dict(
        result=result,
        receiver_coordinates=receiver_coordinates,
        receiver_player=receiver_player,
        receive_timestamp=parse_str_ts(raw_event),
        qualifiers=qualifiers,
    )


def _parse_shot(raw_event: Dict, previous_event: Dict = None) -> Dict:
    outcome_id = raw_event["type"]
    if outcome_id in GOAL_EVENTS:
        result = ShotResult.GOAL
    elif outcome_id in (
        DF_EVENT_TYPE_SHOT_OFF_TARGET,
        DF_EVENT_TYPE_SHOT_PENALTY_OFF_TARGET,
    ):
        result = ShotResult.OFF_TARGET
    elif outcome_id in (
        DF_EVENT_TYPE_SHOT_POST,
        DF_EVENT_TYPE_SHOT_PENALTY_POST,
    ):
        result = ShotResult.POST
    elif outcome_id in (
        DF_EVENT_TYPE_SHOT_SAVED,
        DF_EVENT_TYPE_SHOT_PENALTY_SAVED,
    ):
        result = ShotResult.SAVED
    else:
        raise Exception(f"Unknown shot outcome: {outcome_id}")

    qualifiers = _get_event_qualifiers(raw_event, previous_event)

    return dict(
        result=result,
        qualifiers=qualifiers,
    )


def _parse_card(raw_event: Dict) -> Dict:
    card_id = raw_event["type"]
    if card_id == DF_EVENT_TYPE_RED_CARD:
        card_type = CardType.RED
    elif card_id == DF_EVENT_TYPE_RED_CARD_DOUBLE_YELLOW:
        card_type = CardType.SECOND_YELLOW
    elif card_id == DF_EVENT_TYPE_YELLOW_CARD:
        card_type = CardType.FIRST_YELLOW
    else:
        raise Exception(f"Unknown card id {card_id}")

    return dict(card_type=card_type)


def _parse_substitution(raw_event: Dict, team: Team) -> Dict:
    player = team.get_player_by_id(raw_event["offId"])
    replacement_player = team.get_player_by_id(raw_event["inId"])

    return dict(player=player, replacement_player=replacement_player)


def _include_event(event: Event, wanted_event_types: List) -> bool:
    return not wanted_event_types or event.event_type in wanted_event_types


class DatafactorySerializer(EventDataSerializer):
    @staticmethod
    def __validate_inputs(inputs: Dict[str, Readable]):
        if "event_data" not in inputs:
            raise ValueError("Please specify a value for input 'event_data'")

    def deserialize(
        self, inputs: Dict[str, Readable], options: Dict = None
    ) -> EventDataset:
        """
        Deserialize Datafactory event data into a `EventDataset`.

        Parameters
        ----------
        inputs : dict
            input `event_data` should point to a `Readable` object containing
            the 'json' formatted event data.
        options : dict
            Options for deserialization of the Datafactory file. Possible options are
            `event_types` (list of event types) to specify the event types that
            should be returned.Generic events are barely parsed. This type of event
            can be used to do the parsing yourself.
            Every event has a 'raw_event' attribute which contains the original
            dictionary.
        Returns
        -------
        dataset : EventDataset
        Raises
        ------

        See Also
        --------

        Examples
        --------
        >>> serializer = DatafactorySerializer()
        >>> with open("data.json", "rb") as event_data:
        >>>
        >>>     dataset = serializer.deserialize(
        >>>         inputs={
        >>>             'event_data': event_data,
        >>>         },
        >>>         options={
        >>>             'event_types': ["pass", "shot"]
        >>>         }
        >>>     )
        """
        self.__validate_inputs(inputs)
        if not options:
            options = {}

        from_coordinate_system = build_coordinate_system(
            Provider.DATAFACTORY,
            length=2,
            width=2,
        )

        to_coordinate_system = build_coordinate_system(
            options.get("coordinate_system", Provider.KLOPPY),
            length=2,
            width=2,
        )

        transformer = Transformer(
            from_coordinate_system=from_coordinate_system,
            to_coordinate_system=to_coordinate_system,
        )

        with performance_logging("load data", logger=logger):
            data = json.load(inputs["event_data"])
            match = data["match"]
            score_data = data["scoreStatus"]
            incidences = data["incidences"]
            players_data = data["players"]
            teams_data = data["teams"]

        with performance_logging("parse data", logger=logger):
            teams = []
            scores = []
            team_ids = (
                (Ground.HOME, str(match["homeTeamId"])),
                (Ground.AWAY, str(match["awayTeamId"])),
            )
            for ground, team_id in team_ids:
                team = Team(
                    team_id=team_id,
                    name=teams_data[team_id]["name"],
                    ground=ground,
                )
                team.players = [
                    Player(
                        player_id=player_id,
                        team=team,
                        first_name=player["name"]["first"],
                        last_name=player["name"]["last"],
                        name=player["name"]["shortName"]
                        or player["name"]["nick"],
                        jersey_no=player["squadNo"],
                        starting=not player["substitute"],
                    )
                    for player_id, player in players_data.items()
                    if str(player["teamId"]) == team_id
                ]
                teams.append(team)
                scores.append(score_data.get(team_id, {}).get("score"))
            score = Score(home=scores[0], away=scores[1])

            wanted_event_types = [
                EventType[event_type.upper()]
                for event_type in options.get("event_types", [])
            ]

            # setup periods
            status = incidences.pop(DF_EVENT_CLASS_STATUS)
            # start timestamps are fixed
            start_ts = {1: 0, 2: 45 * 60, 3: 90 * 60, 4: 105 * 60, 5: 120 * 60}
            # check for end status updates to setup periods
            end_event_types = {
                DF_EVENT_TYPE_STATUS_MATCH_END,
                DF_EVENT_TYPE_STATUS_FIRST_HALF_END,
                DF_EVENT_TYPE_STATUS_SECOND_HALF_END,
                DF_EVENT_TYPE_STATUS_FIRST_EXTRA_END,
                DF_EVENT_TYPE_STATUS_SECOND_EXTRA_END,
            }
            periods = {}
            for status_update in status.values():
                if status_update["type"] not in end_event_types:
                    continue
                half = status_update["t"]["half"]
                end_ts = parse_str_ts(status_update)
                periods[half] = Period(
                    id=half,
                    start_timestamp=start_ts[half],
                    end_timestamp=end_ts,
                    attacking_direction=AttackingDirection.HOME_AWAY
                    if half % 2 == 1
                    else AttackingDirection.AWAY_HOME,
                )

            # exclude goals, already listed as shots too
            incidences.pop(DF_EVENT_CLASS_GOALS)
            raw_events = [
                (k, e_id, e)
                for k in incidences
                for e_id, e in incidences[k].items()
            ]
            # sort events by timestamp, event_id
            raw_events.sort(
                key=lambda e: (
                    e[2]["t"]["half"],
                    e[2]["t"]["m"],
                    e[2]["t"]["s"] or 0,
                    e[1],
                )
            )

            home_team, away_team = teams
            events = []
            previous_event = next_event = None
            for i, (e_class, e_id, raw_event) in enumerate(raw_events):
                period = periods.get(raw_event["t"]["half"])
                if period is None:
                    # skip invalid event
                    continue

                timestamp = parse_str_ts(raw_event)
                if (
                    previous_event is not None
                    and previous_event["t"]["half"] != raw_event["t"]["half"]
                ):
                    previous_event = None
                next_event = (
                    raw_events[i + 1][2] if i + 1 < len(raw_events) else None
                )

                team, player = _get_team_and_player(
                    raw_event, home_team, away_team
                )

                event_base_kwargs = dict(
                    # from DataRecord
                    period=period,
                    timestamp=timestamp,
                    ball_owning_team=team,
                    ball_state=BallState.ALIVE,
                    # from Event
                    event_id=e_id,
                    team=team,
                    player=player,
                    coordinates=(
                        _parse_coordinates(raw_event["coord"]["1"])
                        if "coord" in raw_event
                        else None
                    ),
                    raw_event=raw_event,
                    result=None,
                    qualifiers=None,
                )

                if e_class in DF_EVENT_CLASS_PASSES:
                    pass_event_kwargs = _parse_pass(
                        raw_event=raw_event,
                        team=team,
                        previous_event=previous_event,
                        next_event=next_event,
                    )
                    event_base_kwargs.update(pass_event_kwargs)
                    event = PassEvent.create(**event_base_kwargs)

                elif e_class == DF_EVENT_CLASS_SHOTS:
                    shot_event_kwargs = _parse_shot(
                        raw_event=raw_event,
                        previous_event=previous_event,
                    )
                    event_base_kwargs.update(shot_event_kwargs)
                    event = ShotEvent.create(**event_base_kwargs)

                elif e_class == DF_EVENT_CLASS_STEALINGS:
                    event = RecoveryEvent.create(**event_base_kwargs)

                elif e_class == DF_EVENT_CLASS_FOULS:
                    # NOTE: could use qualifiers? (hand, foul, penalty?)
                    # switch possession team
                    event_base_kwargs["ball_owning_team"] = (
                        home_team if team == away_team else away_team
                    )
                    event = FoulCommittedEvent.create(**event_base_kwargs)

                elif e_class in DF_EVENT_CLASS_CARDS:
                    card_kwargs = _parse_card(
                        raw_event=raw_event,
                    )
                    event_base_kwargs.update(card_kwargs)
                    event = CardEvent.create(**event_base_kwargs)

                elif e_class == DF_EVENT_CLASS_SUBSTITUTIONS:
                    substitution_event_kwargs = _parse_substitution(
                        raw_event=raw_event, team=team
                    )
                    event_base_kwargs.update(substitution_event_kwargs)
                    event = SubstitutionEvent.create(**event_base_kwargs)

                else:
                    # otherwise, a generic event
                    event = GenericEvent.create(
                        event_name=e_class,
                        **event_base_kwargs,
                    )

                # check if the event implies ball was out of the field and add a synthetic out event
                if raw_event["type"] in BALL_OUT_EVENTS:
                    ball_out_event = BallOutEvent.create(
                        # from DataRecord
                        period=period,
                        timestamp=timestamp,
                        ball_owning_team=team,
                        ball_state=BallState.DEAD,
                        # from Event
                        event_id=e_id,
                        team=team,
                        player=player,
                        coordinates=event.coordinates,
                        raw_event=raw_event,
                        result=None,
                        qualifiers=None,
                    )
                    if _include_event(event, wanted_event_types):
                        events.append(
                            transformer.transform_event(ball_out_event)
                        )

                if _include_event(event, wanted_event_types):
                    events.append(transformer.transform_event(event))

                # only consider as a previous_event a ball-in-play event
                if e_class not in (
                    DF_EVENT_CLASS_YELLOW_CARDS,
                    DF_EVENT_CLASS_RED_CARDS,
                    DF_EVENT_CLASS_SUBSTITUTIONS,
                    DF_EVENT_CLASS_PENALTY_SHOOTOUT,
                ):
                    previous_event = raw_event

        metadata = Metadata(
            teams=teams,
            periods=sorted(periods.values(), key=lambda p: p.id),
            pitch_dimensions=to_coordinate_system.pitch_dimensions,
            frame_rate=None,
            orientation=Orientation.HOME_TEAM,
            flags=DatasetFlag.BALL_OWNING_TEAM,
            score=score,
            provider=Provider.DATAFACTORY,
            coordinate_system=to_coordinate_system,
        )

        return EventDataset(
            metadata=metadata,
            records=events,
        )

    def serialize(self, data_set: EventDataset) -> Tuple[str, str]:
        raise NotImplementedError
