from typing import List, NamedTuple, Any
import numpy as np
import cv2
import torch
from safetensors.torch import load_file

from diffusers import UNet2DConditionModel
from diffusers.models.unet_2d_condition import UNet2DConditionOutput

import library.model_util as model_util


class ControlNetInfo(NamedTuple):
  unet: Any
  net: Any
  prep: Any
  weight: float
  ratio: float


class ControlNet(torch.nn.Module):
  def __init__(self) -> None:
    super().__init__()

    # make control model
    self.control_model = torch.nn.Module()

    dims = [320, 320, 320, 320, 640, 640, 640, 1280, 1280, 1280, 1280, 1280]
    zero_convs = torch.nn.ModuleList()
    for i, dim in enumerate(dims):
      sub_list = torch.nn.ModuleList([torch.nn.Conv2d(dim, dim, 1)])
      zero_convs.append(sub_list)
    self.control_model.add_module("zero_convs", zero_convs)

    middle_block_out = torch.nn.Conv2d(1280, 1280, 1)
    self.control_model.add_module("middle_block_out", torch.nn.ModuleList([middle_block_out]))

    dims = [16, 16, 32, 32, 96, 96, 256, 320]
    strides = [1, 1, 2, 1, 2, 1, 2, 1]
    prev_dim = 3
    input_hint_block = torch.nn.Sequential()
    for i, (dim, stride) in enumerate(zip(dims, strides)):
      input_hint_block.append(torch.nn.Conv2d(prev_dim, dim, 3, stride, 1))
      if i < len(dims) - 1:
        input_hint_block.append(torch.nn.SiLU())
      prev_dim = dim
    self.control_model.add_module("input_hint_block", input_hint_block)


def load_control_net(v2, unet, model):
  device = unet.device

  # control sdからキー変換しつつU-Netに対応する部分のみ取り出し、DiffusersのU-Netに読み込む
  # state dictを読み込む
  print(f"ControlNet: loading control SD model : {model}")

  if model_util.is_safetensors(model):
    ctrl_sd_sd = load_file(model)
  else:
    ctrl_sd_sd = torch.load(model, map_location='cpu')
    ctrl_sd_sd = ctrl_sd_sd.pop("state_dict", ctrl_sd_sd)

  # 重みをU-Netに読み込めるようにする。ControlNetはSD版のstate dictなので、それを読み込む
  is_difference = "difference" in ctrl_sd_sd
  print("ControlNet: loading difference:", is_difference)

  # ControlNetには存在しないキーがあるので、まず現在のU-NetでSD版の全keyを作っておく
  # またTransfer Controlの元weightとなる
  ctrl_unet_sd_sd = model_util.convert_unet_state_dict_to_sd(v2, unet.state_dict())

  # 元のU-Netに影響しないようにコピーする。またprefixが付いていないので付ける
  for key in list(ctrl_unet_sd_sd.keys()):
    ctrl_unet_sd_sd["model.diffusion_model." + key] = ctrl_unet_sd_sd.pop(key).clone()

  zero_conv_sd = {}
  for key in list(ctrl_sd_sd.keys()):
    if key.startswith("control_"):
      unet_key = "model.diffusion_" + key[len("control_"):]
      if unet_key not in ctrl_unet_sd_sd:               # zero conv
        zero_conv_sd[key] = ctrl_sd_sd[key]
        continue
      if is_difference:                                 # Transfer Control
        ctrl_unet_sd_sd[unet_key] += ctrl_sd_sd[key].to(device, dtype=unet.dtype)
      else:
        ctrl_unet_sd_sd[unet_key] = ctrl_sd_sd[key].to(device, dtype=unet.dtype)

  unet_config = model_util.create_unet_diffusers_config(v2)
  ctrl_unet_du_sd = model_util.convert_ldm_unet_checkpoint(v2, ctrl_unet_sd_sd, unet_config)    # DiffUsers版ControlNetのstate dict

  # ControlNetのU-Netを作成する
  ctrl_unet = UNet2DConditionModel(**unet_config)
  info = ctrl_unet.load_state_dict(ctrl_unet_du_sd)
  print("ControlNet: loading Control U-Net:", info)

  # U-Net以外のControlNetを作成する
  # TODO support middle only
  ctrl_net = ControlNet()
  info = ctrl_net.load_state_dict(zero_conv_sd)
  print("ControlNet: loading ControlNet:", info)

  ctrl_unet.to(unet.device, dtype=unet.dtype)
  ctrl_net.to(unet.device, dtype=unet.dtype)
  return ctrl_unet, ctrl_net


def load_preprocess(prep_type: str):
  if prep_type is None or prep_type.lower() == "none":
    return None

  if prep_type.startswith("canny"):
    args = prep_type.split("_")
    th1 = int(args[1]) if len(args) >= 2 else 63
    th2 = int(args[2]) if len(args) >= 3 else 191

    def canny(img):
      img = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
      return cv2.Canny(img, th1, th2)
    return canny

  print("Unsupported prep type:", prep_type)
  return None


def preprocess_ctrl_net_hint_image(image):
  image = np.array(image).astype(np.float32) / 255.0
  # ControlNetのサンプルはcv2を使っているが、読み込みはGradioなので実はRGBになっている
  # image = image[:, :, ::-1].copy()                         # rgb to bgr
  image = image[None].transpose(0, 3, 1, 2)       # nchw
  image = torch.from_numpy(image)
  return image                              # 0 to 1


def get_guided_hints(control_nets: List[ControlNetInfo], num_latent_input, b_size, hints):
  guided_hints = []
  for i, cnet_info in enumerate(control_nets):
    # hintは 1枚目の画像のcnet1, 1枚目の画像のcnet2, 1枚目の画像のcnet3, 2枚目の画像のcnet1, 2枚目の画像のcnet2 ... と並んでいること
    b_hints = []
    if len(hints) == 1:           # すべて同じ画像をhintとして使う
      hint = hints[0]
      if cnet_info.prep is not None:
        hint = cnet_info.prep(hint)
      hint = preprocess_ctrl_net_hint_image(hint)
      b_hints = [hint for _ in range(b_size)]
    else:
      for bi in range(b_size):
        hint = hints[(bi * len(control_nets) + i) % len(hints)]
        if cnet_info.prep is not None:
          hint = cnet_info.prep(hint)
        hint = preprocess_ctrl_net_hint_image(hint)
        b_hints.append(hint)
    b_hints = torch.cat(b_hints, dim=0)
    b_hints = b_hints.to(cnet_info.unet.device, dtype=cnet_info.unet.dtype)

    guided_hint = cnet_info.net.control_model.input_hint_block(b_hints)
    guided_hints.append(guided_hint)
  return guided_hints


def call_unet_and_control_net(step, num_latent_input, original_unet, control_nets: List[ControlNetInfo], guided_hints, current_ratio, sample, timestep, encoder_hidden_states):
  # ControlNet
  # 複数のControlNetの場合は、出力をマージするのではなく交互に適用する
  cnet_cnt = len(control_nets)
  cnet_idx = step % cnet_cnt
  cnet_info = control_nets[cnet_idx]

  # print(current_ratio, cnet_info.prep, cnet_info.weight, cnet_info.ratio)
  if cnet_info.ratio < current_ratio:
    return original_unet(sample, timestep, encoder_hidden_states)

  guided_hint = guided_hints[cnet_idx]
  guided_hint = guided_hint.repeat((num_latent_input, 1, 1, 1))
  outs = unet_forward(True, cnet_info.net, cnet_info.unet, guided_hint, None, sample, timestep, encoder_hidden_states)
  outs = [o * cnet_info.weight for o in outs]

  # U-Net
  return unet_forward(False, cnet_info.net, original_unet, None, outs, sample, timestep, encoder_hidden_states)


"""
  # これはmergeのバージョン
  # ControlNet
  cnet_outs_list = []
  for i, cnet_info in enumerate(control_nets):
    # print(current_ratio, cnet_info.prep, cnet_info.weight, cnet_info.ratio)
    if cnet_info.ratio < current_ratio:
      continue
    guided_hint = guided_hints[i]
    outs = unet_forward(True, cnet_info.net, cnet_info.unet, guided_hint, None, sample, timestep, encoder_hidden_states)
    for i in range(len(outs)):
      outs[i] *= cnet_info.weight

    cnet_outs_list.append(outs)

  count = len(cnet_outs_list)
  if count == 0:
    return original_unet(sample, timestep, encoder_hidden_states)

  # sum of controlnets
  for i in range(1, count):
    cnet_outs_list[0] += cnet_outs_list[i]

  # U-Net
  return unet_forward(False, cnet_info.net, original_unet, None, cnet_outs_list[0], sample, timestep, encoder_hidden_states)
"""


def unet_forward(is_control_net, control_net: ControlNet, unet: UNet2DConditionModel, guided_hint, ctrl_outs, sample, timestep, encoder_hidden_states):
  # copy from UNet2DConditionModel
  default_overall_up_factor = 2**unet.num_upsamplers

  forward_upsample_size = False
  upsample_size = None

  if any(s % default_overall_up_factor != 0 for s in sample.shape[-2:]):
    print("Forward upsample size to force interpolation output size.")
    forward_upsample_size = True

  # 0. center input if necessary
  if unet.config.center_input_sample:
    sample = 2 * sample - 1.0

  # 1. time
  timesteps = timestep
  if not torch.is_tensor(timesteps):
    # TODO: this requires sync between CPU and GPU. So try to pass timesteps as tensors if you can
    # This would be a good case for the `match` statement (Python 3.10+)
    is_mps = sample.device.type == "mps"
    if isinstance(timestep, float):
      dtype = torch.float32 if is_mps else torch.float64
    else:
      dtype = torch.int32 if is_mps else torch.int64
    timesteps = torch.tensor([timesteps], dtype=dtype, device=sample.device)
  elif len(timesteps.shape) == 0:
    timesteps = timesteps[None].to(sample.device)

  # broadcast to batch dimension in a way that's compatible with ONNX/Core ML
  timesteps = timesteps.expand(sample.shape[0])

  t_emb = unet.time_proj(timesteps)

  # timesteps does not contain any weights and will always return f32 tensors
  # but time_embedding might actually be running in fp16. so we need to cast here.
  # there might be better ways to encapsulate this.
  t_emb = t_emb.to(dtype=unet.dtype)
  emb = unet.time_embedding(t_emb)

  outs = []                     # output of ControlNet
  zc_idx = 0

  # 2. pre-process
  sample = unet.conv_in(sample)
  if is_control_net:
    sample += guided_hint
    outs.append(control_net.control_model.zero_convs[zc_idx][0](sample))  # , emb, encoder_hidden_states))
    zc_idx += 1

  # 3. down
  down_block_res_samples = (sample,)
  for downsample_block in unet.down_blocks:
    if hasattr(downsample_block, "has_cross_attention") and downsample_block.has_cross_attention:
      sample, res_samples = downsample_block(
          hidden_states=sample,
          temb=emb,
          encoder_hidden_states=encoder_hidden_states,
      )
    else:
      sample, res_samples = downsample_block(hidden_states=sample, temb=emb)
    if is_control_net:
      for rs in res_samples:
        outs.append(control_net.control_model.zero_convs[zc_idx][0](rs))  # , emb, encoder_hidden_states))
        zc_idx += 1

    down_block_res_samples += res_samples

  # 4. mid
  sample = unet.mid_block(sample, emb, encoder_hidden_states=encoder_hidden_states)
  if is_control_net:
    outs.append(control_net.control_model.middle_block_out[0](sample))
    return outs

  if not is_control_net:
    sample += ctrl_outs.pop()

  # 5. up
  for i, upsample_block in enumerate(unet.up_blocks):
    is_final_block = i == len(unet.up_blocks) - 1

    res_samples = down_block_res_samples[-len(upsample_block.resnets):]
    down_block_res_samples = down_block_res_samples[: -len(upsample_block.resnets)]

    if not is_control_net and len(ctrl_outs) > 0:
      res_samples = list(res_samples)
      apply_ctrl_outs = ctrl_outs[-len(res_samples):]
      ctrl_outs = ctrl_outs[:-len(res_samples)]
      for j in range(len(res_samples)):
        res_samples[j] = res_samples[j] + apply_ctrl_outs[j]
      res_samples = tuple(res_samples)

    # if we have not reached the final block and need to forward the
    # upsample size, we do it here
    if not is_final_block and forward_upsample_size:
      upsample_size = down_block_res_samples[-1].shape[2:]

    if hasattr(upsample_block, "has_cross_attention") and upsample_block.has_cross_attention:
      sample = upsample_block(
          hidden_states=sample,
          temb=emb,
          res_hidden_states_tuple=res_samples,
          encoder_hidden_states=encoder_hidden_states,
          upsample_size=upsample_size,
      )
    else:
      sample = upsample_block(
          hidden_states=sample, temb=emb, res_hidden_states_tuple=res_samples, upsample_size=upsample_size
      )
  # 6. post-process
  sample = unet.conv_norm_out(sample)
  sample = unet.conv_act(sample)
  sample = unet.conv_out(sample)

  return UNet2DConditionOutput(sample=sample)
