import torch
import torch.nn as nn
from .operations import *
from net.layer import FunctionWrapper, BasicAdd, BasicReshape
from functools import partial
from .utils import drop_path
import networkx as nx
from .genotypes import DARTS, NASNet, AmoebaNet


class Cell(nn.Module):

    def __init__(self, genotype, C_prev_prev, C_prev, C, reduction, reduction_prev):
        super(Cell, self).__init__()
        print(C_prev_prev, C_prev, C)

        if reduction_prev:
            self.preprocess0 = FactorizedReduce(C_prev_prev, C)
        else:
            self.preprocess0 = ReLUConvBN(C_prev_prev, C, 1, 1, 0)
        self.preprocess1 = ReLUConvBN(C_prev, C, 1, 1, 0)

        if reduction:
            op_names, indices = zip(*genotype.reduce)
            concat = genotype.reduce_concat
        else:
            op_names, indices = zip(*genotype.normal)
            concat = genotype.normal_concat
        self._compile(C, op_names, indices, concat, reduction)

    def _compile(self, C, op_names, indices, concat, reduction):
        assert len(op_names) == len(indices)
        self._steps = len(op_names) // 2
        self._concat = concat
        self.multiplier = len(concat)

        self._ops = nn.ModuleList()
        for name, index in zip(op_names, indices):
            stride = 2 if reduction and index < 2 else 1
            op = OPS[name](C, stride, True)
            self._ops += [op]
        self._indices = indices

    def forward(self, s0, s1, drop_prob):
        s0 = self.preprocess0(s0)
        s1 = self.preprocess1(s1)

        states = [s0, s1]
        for i in range(self._steps):
            h1 = states[self._indices[2 * i]]
            h2 = states[self._indices[2 * i + 1]]
            op1 = self._ops[2 * i]
            op2 = self._ops[2 * i + 1]
            h1 = op1(h1)
            h2 = op2(h2)
            if self.training and drop_prob > 0.:
                if not isinstance(op1, Identity):
                    h1 = drop_path(h1, drop_prob)
                if not isinstance(op2, Identity):
                    h2 = drop_path(h2, drop_prob)
            s = h1 + h2
            states += [s]
        return torch.cat([states[i] for i in self._concat], dim=1)

    def parse_graph(self, G, s0, s1, input_id_0, input_id_1, vertex_id, drop_prob):

        G, s0, vertex_id = self.preprocess0.parse_graph(G, s0, input_id_0, vertex_id)
        s0_id = vertex_id
        G, s1, vertex_id = self.preprocess1.parse_graph(G, s1, input_id_1, vertex_id)
        s1_id = vertex_id

        states = [s0, s1]
        states_ids = [s0_id, s1_id]

        for i in range(self._steps):
            h1 = states[self._indices[2 * i]]
            h2 = states[self._indices[2 * i + 1]]
            h1_id = states_ids[self._indices[2 * i]]
            h2_id = states_ids[self._indices[2 * i + 1]]
            op1 = self._ops[2 * i]
            op2 = self._ops[2 * i + 1]
            if hasattr(op1, 'parse_graph'):
                G, h1, vertex_id = op1.parse_graph(G, h1, h1_id, vertex_id)
                h1_id = vertex_id
            else:
                h1 = op1(h1)
                G.add_node(vertex_id + 1, cost=h1.numel(), shape=list(h1.shape))
                G.add_edge(h1_id, vertex_id + 1, cost=0, module=op1)
                vertex_id += 1
                h1_id = vertex_id

            if hasattr(op2, 'parse_graph'):
                G, h2, vertex_id = op2.parse_graph(G, h2, h2_id, vertex_id)
                h2_id = vertex_id
            else:
                h2 = op2(h2)
                G.add_node(vertex_id + 1, cost=h2.numel(), shape=list(h2.shape))
                G.add_edge(h2_id, vertex_id + 1, cost=0, module=op2)
                vertex_id += 1
                h2_id = vertex_id

            if self.training and drop_prob > 0.:
                if not isinstance(op1, Identity):
                    op = FunctionWrapper(partial(drop_path, drop_prob=drop_prob))
                    h1 = op(h1)
                    G.add_node(vertex_id + 1, cost=h1.numel(), shape=list(h1.shape))
                    G.add_edge(h1_id, vertex_id + 1, cost=0, module=op)
                    vertex_id += 1
                    h1_id = vertex_id
                if not isinstance(op2, Identity):
                    op = FunctionWrapper(partial(drop_path, drop_prob=drop_prob))
                    h2 = op(h2)
                    G.add_node(vertex_id + 1, cost=h2.numel(), shape=list(h2.shape))
                    G.add_edge(h2_id, vertex_id + 1, cost=0, module=op)
                    vertex_id += 1
                    h2_id = vertex_id
            op = BasicAdd()
            identity = BasicIdentity()
            s = op([h1, h2])
            G.add_node(vertex_id + 1, cost=s.numel(), transition=op, shape=list(s.shape))
            G.nodes[vertex_id + 1]['transition_input_order'] = []
            for id in [h1_id, h2_id]:
                edge_id = G.add_edge(id, vertex_id + 1, cost=0, module=identity)
                G.nodes[vertex_id + 1]['transition_input_order'].append((id, edge_id))
            vertex_id += 1
            states += [s]
            states_ids.append(vertex_id)

        op = BasicCatDim1()
        identity = BasicIdentity()
        states_concat = [states[i] for i in self._concat]
        states_concat_ids = [states_ids[i] for i in self._concat]
        x = op(states_concat)
        G.add_node(vertex_id + 1, cost=x.numel(), transition=op, shape=list(x.shape))
        G.nodes[vertex_id + 1]['transition_input_order'] = []
        for id in states_concat_ids:
            edge_id = G.add_edge(id, vertex_id + 1, cost=0, module=identity)
            G.nodes[vertex_id + 1]['transition_input_order'].append((id, edge_id))
        vertex_id += 1


        return G, x, vertex_id


class AuxiliaryHeadCIFAR(nn.Module):

    def __init__(self, C, num_classes):
        """assuming input size 8x8"""
        super(AuxiliaryHeadCIFAR, self).__init__()
        self.features = nn.Sequential(
            nn.ReLU(inplace=True),
            nn.AvgPool2d(5, stride=3, padding=0, count_include_pad=False),  # image size = 2 x 2
            nn.Conv2d(C, 128, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 768, 2, bias=False),
            nn.BatchNorm2d(768),
            nn.ReLU(inplace=True)
        )
        self.classifier = nn.Linear(768, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x.view(x.size(0), -1))
        return x


class AuxiliaryHeadImageNet(nn.Module):

    def __init__(self, C, num_classes):
        """assuming input size 14x14"""
        super(AuxiliaryHeadImageNet, self).__init__()
        self.features = nn.Sequential(
            nn.ReLU(inplace=True),
            nn.AvgPool2d(5, stride=2, padding=0, count_include_pad=False),
            nn.Conv2d(C, 128, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(inplace=True),
            nn.Conv2d(128, 768, 2, bias=False),
            # NOTE: This batchnorm was omitted in my earlier implementation due to a typo.
            # Commenting it out for consistency with the experiments in the paper.
            # nn.BatchNorm2d(768),
            nn.ReLU(inplace=True)
        )
        self.classifier = nn.Linear(768, num_classes)

    def forward(self, x):
        x = self.features(x)
        x = self.classifier(x.view(x.size(0), -1))
        return x


class NetworkCIFAR(nn.Module):

    def __init__(self, C, num_classes, layers, auxiliary, genotype, drop_path_prob=0):
        super(NetworkCIFAR, self).__init__()
        self._layers = layers
        self._auxiliary = auxiliary

        stem_multiplier = 3
        C_curr = stem_multiplier * C
        self.stem = nn.Sequential(
            nn.Conv2d(3, C_curr, 3, padding=1, bias=False),
            nn.BatchNorm2d(C_curr)
        )

        C_prev_prev, C_prev, C_curr = C_curr, C_curr, C
        self.cells = nn.ModuleList()
        reduction_prev = False
        for i in range(layers):
            if i in [layers // 3, 2 * layers // 3]:
                C_curr *= 2
                reduction = True
            else:
                reduction = False
            cell = Cell(genotype, C_prev_prev, C_prev, C_curr, reduction, reduction_prev)
            reduction_prev = reduction
            self.cells += [cell]
            C_prev_prev, C_prev = C_prev, cell.multiplier * C_curr
            if i == 2 * layers // 3:
                C_to_auxiliary = C_prev

        if auxiliary:
            self.auxiliary_head = AuxiliaryHeadCIFAR(C_to_auxiliary, num_classes)
        self.global_pooling = nn.AdaptiveAvgPool2d(1)
        self.classifier = nn.Linear(C_prev, num_classes)
        self.drop_path_prob = drop_path_prob

    def forward(self, input):
        logits_aux = None
        s0 = s1 = self.stem(input)
        for i, cell in enumerate(self.cells):
            s0, s1 = s1, cell(s0, s1, self.drop_path_prob)
            if i == 2 * self._layers // 3:
                if self._auxiliary and self.training:
                    logits_aux = self.auxiliary_head(s1)
        out = self.global_pooling(s1)
        logits = self.classifier(out.view(out.size(0), -1))
        # return logits, logits_aux
        return logits

    def parse_graph(self, x):
        # todo: assuming auxiliary is turned off for now
        # todo: add packing layer for multi-input and multi-output network
        G = nx.MultiDiGraph()
        source = 0
        vertex_id = 0
        G.add_node(vertex_id, cost=x.numel(), shape=list(x.shape))

        op = self.stem[0]
        x = op(x)
        G.add_node(vertex_id + 1, cost=x.numel(), shape=list(x.shape))
        G.add_edge(vertex_id, vertex_id + 1, cost=0, module=op)
        vertex_id += 1

        op = self.stem[1]
        x = op(x)
        G.add_node(vertex_id + 1, cost=x.numel(), shape=list(x.shape))
        G.add_edge(vertex_id, vertex_id + 1, cost=0, module=op)
        vertex_id += 1

        s0 = s1 = x
        s0_id = s1_id = vertex_id

        for i, cell in enumerate(self.cells):
            G, x, vertex_id = cell.parse_graph(G, s0, s1, s0_id, s1_id, vertex_id, self.drop_path_prob)
            s0, s1 = s1, x
            s0_id, s1_id = s1_id, vertex_id

        op = self.global_pooling
        x = op(s1)
        G.add_node(vertex_id + 1, cost=x.numel(), shape=list(x.shape))
        G.add_edge(s1_id, vertex_id + 1, cost=0, module=op)
        vertex_id += 1

        reshape = BasicReshape()
        op = nn.Sequential(reshape, self.classifier)
        x = op(x)
        G.add_node(vertex_id + 1, cost=x.numel(), shape=list(x.shape))
        G.add_edge(vertex_id, vertex_id + 1, cost=0, module=op)
        vertex_id += 1

        target = vertex_id

        return G, source, target

class NetworkImageNet(nn.Module):

    def __init__(self, C, num_classes, layers, auxiliary, genotype):
        super(NetworkImageNet, self).__init__()
        self._layers = layers
        self._auxiliary = auxiliary

        self.stem0 = nn.Sequential(
            nn.Conv2d(3, C // 2, kernel_size=3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(C // 2),
            nn.ReLU(inplace=True),
            nn.Conv2d(C // 2, C, 3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(C),
        )

        self.stem1 = nn.Sequential(
            nn.ReLU(inplace=True),
            nn.Conv2d(C, C, 3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(C),
        )

        C_prev_prev, C_prev, C_curr = C, C, C

        self.cells = nn.ModuleList()
        reduction_prev = True
        for i in range(layers):
            if i in [layers // 3, 2 * layers // 3]:
                C_curr *= 2
                reduction = True
            else:
                reduction = False
            cell = Cell(genotype, C_prev_prev, C_prev, C_curr, reduction, reduction_prev)
            reduction_prev = reduction
            self.cells += [cell]
            C_prev_prev, C_prev = C_prev, cell.multiplier * C_curr
            if i == 2 * layers // 3:
                C_to_auxiliary = C_prev

        if auxiliary:
            self.auxiliary_head = AuxiliaryHeadImageNet(C_to_auxiliary, num_classes)
        self.global_pooling = nn.AvgPool2d(7)
        self.classifier = nn.Linear(C_prev, num_classes)

    def forward(self, input):
        logits_aux = None
        s0 = self.stem0(input)
        s1 = self.stem1(s0)
        for i, cell in enumerate(self.cells):
            s0, s1 = s1, cell(s0, s1, self.drop_path_prob)
            if i == 2 * self._layers // 3:
                if self._auxiliary and self.training:
                    logits_aux = self.auxiliary_head(s1)
        out = self.global_pooling(s1)
        logits = self.classifier(out.view(out.size(0), -1))
        return logits, logits_aux

def darts_cifar10():
    return NetworkCIFAR(36, 10, 20, False, DARTS)

def nasnet_cifar10():
    return NetworkCIFAR(36, 10, 20, False, NASNet)

def amoebanet_cifar10():
    return NetworkCIFAR(36, 10, 20, False, AmoebaNet)