import copy
import json
import logging
import operator
from operator import itemgetter

import numpy as np
import pandas as pd
import requests

from .animation_key_frames import DeformAnimKeys

logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO)


class ParseqAnimKeys():
    def __init__(self, parseq_args, anim_args):

        # Resolve manifest either directly from supplied value
        # or via supplied URL
        manifestOrUrl = parseq_args.parseq_manifest.strip()
        if (manifestOrUrl.startswith('http')):
            logging.info(f"Loading Parseq manifest from URL: {manifestOrUrl}")
            try:
                body = requests.get(manifestOrUrl).text
                logging.debug(f"Loaded remote manifest: {body}")
                self.parseq_json = json.loads(body)

                # Add the parseq manifest without the detailed frame data to parseq_args.
                # This ensures it will be saved in the settings file, so that you can always
                # see exactly what parseq prompts and keyframes were used, even if what the URL
                # points to changes.
                parseq_args.fetched_parseq_manifest_summary = copy.deepcopy(self.parseq_json)
                if parseq_args.fetched_parseq_manifest_summary['rendered_frames']:
                    del parseq_args.fetched_parseq_manifest_summary['rendered_frames']
                if parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']:
                    del parseq_args.fetched_parseq_manifest_summary['rendered_frames_meta']

            except Exception as e:
                logging.error(f"Unable to load Parseq manifest from URL: {manifestOrUrl}")
                raise e
        else:
            self.parseq_json = json.loads(manifestOrUrl)

        self.default_anim_keys = DeformAnimKeys(anim_args)
        self.rendered_frames = self.parseq_json['rendered_frames']       
        self.max_frame = self.get_max('frame')
        count_defined_frames = len(self.rendered_frames)        
        expected_defined_frames = self.max_frame+1 # frames are 0-indexed

        self.required_frames = anim_args.max_frames

        if (expected_defined_frames != count_defined_frames): 
            logging.warning(f"There may be duplicated or missing frame data in the Parseq input: expected {expected_defined_frames} frames including frame 0 because the highest frame number is {self.max_frame}, but there are {count_defined_frames} frames defined.")

        if (anim_args.max_frames > count_defined_frames):
            logging.info(f"Parseq data defines {count_defined_frames} frames, but the requested animation is {anim_args.max_frames} frames. The last Parseq frame definition will be duplicated to match the expected frame count.")
        if (anim_args.max_frames < count_defined_frames):
            logging.info(f"Parseq data defines {count_defined_frames} frames, but the requested animation is {anim_args.max_frames} frames. The last Parseq frame definitions will be ignored.")
        else:
            logging.info(f"Parseq data defines {count_defined_frames} frames.")

        # Parseq treats input values as absolute values. So if you want to 
        # progressively rotate 180 degrees over 4 frames, you specify: 45, 90, 135, 180.
        # However, many animation parameters are relative to the previous frame if there is enough
        # loopback strength. So if you want to rotate 180 degrees over 5 frames, the animation engine expects:
        # 45, 45, 45, 45. Therefore, for such parameter, we use the fact that Parseq supplies delta values.
        optional_delta = '_delta' if parseq_args.parseq_use_deltas else ''
        self.angle_series = self.parseq_to_anim_series('angle' + optional_delta)
        self.zoom_series = self.parseq_to_anim_series('zoom' + optional_delta)        
        self.translation_x_series = self.parseq_to_anim_series('translation_x' + optional_delta)
        self.translation_y_series = self.parseq_to_anim_series('translation_y' + optional_delta)
        self.translation_z_series = self.parseq_to_anim_series('translation_z' + optional_delta)
        self.rotation_3d_x_series = self.parseq_to_anim_series('rotation_3d_x' + optional_delta)
        self.rotation_3d_y_series = self.parseq_to_anim_series('rotation_3d_y' + optional_delta)
        self.rotation_3d_z_series = self.parseq_to_anim_series('rotation_3d_z' + optional_delta)
        self.perspective_flip_theta_series = self.parseq_to_anim_series('perspective_flip_theta' + optional_delta)
        self.perspective_flip_phi_series = self.parseq_to_anim_series('perspective_flip_phi' + optional_delta)
        self.perspective_flip_gamma_series = self.parseq_to_anim_series('perspective_flip_gamma' + optional_delta)
 
        # Non-motion animation args
        self.perspective_flip_fv_series = self.parseq_to_anim_series('perspective_flip_fv')
        self.noise_schedule_series = self.parseq_to_anim_series('noise')
        self.strength_schedule_series = self.parseq_to_anim_series('strength')
        self.sampler_schedule_series = self.parseq_to_anim_series('sampler_schedule')
        self.contrast_schedule_series = self.parseq_to_anim_series('contrast')
        self.cfg_scale_schedule_series = self.parseq_to_anim_series('scale')
        self.steps_schedule_series = self.parseq_to_anim_series("steps_schedule")
        self.seed_schedule_series = self.parseq_to_anim_series('seed')
        self.fov_series = self.parseq_to_anim_series('fov')
        self.near_series = self.parseq_to_anim_series('near')
        self.far_series = self.parseq_to_anim_series('far')
        self.prompts = self.parseq_to_anim_series('deforum_prompt') # formatted as "{positive} --neg {negative}"
        self.subseed_series = self.parseq_to_anim_series('subseed')
        self.subseed_strength_series = self.parseq_to_anim_series('subseed_strength')
        self.kernel_schedule_series = self.parseq_to_anim_series('antiblur_kernel')
        self.sigma_schedule_series = self.parseq_to_anim_series('antiblur_sigma')
        self.amount_schedule_series = self.parseq_to_anim_series('antiblur_amount')
        self.threshold_schedule_series = self.parseq_to_anim_series('antiblur_threshold')
        
        # Config:
        # TODO this is currently ignored. User must ensure the output FPS set in parseq
        # matches the one set in Deforum to avoid unexpected results.
        self.config_output_fps = self.parseq_json['options']['output_fps']

    def get_max(self, seriesName):
        return max(self.rendered_frames, key=itemgetter(seriesName))[seriesName]

    def parseq_to_anim_series(self, seriesName):
        
        # Check if valus is present in first frame of JSON data. If not, assume it's undefined.
        # The Parseq contract is that the first frame (at least) must define values for all fields.
        try:
            if self.rendered_frames[0][seriesName] is not None:
                logging.info(f"Found {seriesName} in first frame of Parseq data. Assuming it's defined.")
        except KeyError:
            return None

        key_frame_series = pd.Series([np.nan for a in range(self.required_frames)])
        
        for frame in self.rendered_frames:
            frame_idx = frame['frame']
            if frame_idx < self.required_frames:                
                if not np.isnan(key_frame_series[frame_idx]):
                    logging.warning(f"Duplicate frame definition {frame_idx} detected for data {seriesName}. Latest wins.")        
                key_frame_series[frame_idx] = frame[seriesName]

        # If the animation will have more frames than Parseq defines,
        # duplicate final value to match the required frame count.
        while (frame_idx < self.required_frames):
            key_frame_series[frame_idx] = operator.itemgetter(-1)(self.rendered_frames)[seriesName]
            frame_idx += 1

        return key_frame_series

    # fallback to anim_args if the series is not defined in the Parseq data
    def __getattribute__(inst, name):
        try:
            definedField = super(ParseqAnimKeys, inst).__getattribute__(name)
        except AttributeError:
            # No field with this name has been explicitly extracted from the JSON data.
            # It must be a new parameter. Let's see if it's in the raw JSON.

            # parseq doesn't use _series, _schedule or _schedule_series suffixes in the
            # JSON data - remove them.
            strippableSuffixes = ['_series', '_schedule']
            parseqName = name
            while any(parseqName.endswith(suffix) for suffix in strippableSuffixes):
                for suffix in strippableSuffixes:
                    if parseqName.endswith(suffix):
                        parseqName = parseqName[:-len(suffix)]            
            
            # returns None if not defined in Parseq JSON data
            definedField = inst.parseq_to_anim_series(parseqName)
            if (definedField is not None):
                # add the field to the instance so we don't compute it again.
                setattr(inst, name, definedField)

        if (definedField is not None):
            return definedField
        else:
            logging.info(f"Data for {name} not defined in Parseq data (looked for: '{parseqName}'). Falling back to standard Deforum values.")
            return getattr(inst.default_anim_keys, name)

