import os
import pickle as pkl
import random

import dgl

import numpy as np
import torch
from torch.utils.data import DataLoader, Dataset


# Split data into train/eval/test
def split_data(hg, etype_name):
    src, dst = hg.edges(etype=etype_name)
    user_item_src = src.numpy().tolist()
    user_item_dst = dst.numpy().tolist()

    num_link = len(user_item_src)
    pos_label = [1] * num_link
    pos_data = list(zip(user_item_src, user_item_dst, pos_label))

    ui_adj = np.array(hg.adj_external(etype=etype_name).to_dense())
    full_idx = np.where(ui_adj == 0)

    sample = random.sample(range(0, len(full_idx[0])), num_link)
    neg_label = [0] * num_link
    neg_data = list(zip(full_idx[0][sample], full_idx[1][sample], neg_label))

    full_data = pos_data + neg_data
    random.shuffle(full_data)

    train_size = int(len(full_data) * 0.6)
    eval_size = int(len(full_data) * 0.2)
    test_size = len(full_data) - train_size - eval_size
    train_data = full_data[:train_size]
    eval_data = full_data[train_size : train_size + eval_size]
    test_data = full_data[
        train_size + eval_size : train_size + eval_size + test_size
    ]
    train_data = np.array(train_data)
    eval_data = np.array(eval_data)
    test_data = np.array(test_data)

    return train_data, eval_data, test_data


def process_amazon(root_path):
    # User-Item 3584 2753 50903 UIUI
    # Item-View 2753 3857 5694 UIVI
    # Item-Brand 2753 334 2753 UIBI
    # Item-Category 2753 22 5508 UICI

    # Construct graph from raw data.
    # load data of amazon
    data_path = os.path.join(root_path, "Amazon")
    if not (os.path.exists(data_path)):
        print(
            "Can not find amazon in {}, please download the dataset first.".format(
                data_path
            )
        )

    # item_view
    item_view_src = []
    item_view_dst = []
    with open(os.path.join(data_path, "item_view.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split(",")
            item, view = int(_line[0]), int(_line[1])
            item_view_src.append(item)
            item_view_dst.append(view)

    # user_item
    user_item_src = []
    user_item_dst = []
    with open(os.path.join(data_path, "user_item.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split("\t")
            user, item, rate = int(_line[0]), int(_line[1]), int(_line[2])
            if rate > 3:
                user_item_src.append(user)
                user_item_dst.append(item)

    # item_brand
    item_brand_src = []
    item_brand_dst = []
    with open(os.path.join(data_path, "item_brand.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split(",")
            item, brand = int(_line[0]), int(_line[1])
            item_brand_src.append(item)
            item_brand_dst.append(brand)

    # item_category
    item_category_src = []
    item_category_dst = []
    with open(os.path.join(data_path, "item_category.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split(",")
            item, category = int(_line[0]), int(_line[1])
            item_category_src.append(item)
            item_category_dst.append(category)

    # build graph
    hg = dgl.heterograph(
        {
            ("item", "iv", "view"): (item_view_src, item_view_dst),
            ("view", "vi", "item"): (item_view_dst, item_view_src),
            ("user", "ui", "item"): (user_item_src, user_item_dst),
            ("item", "iu", "user"): (user_item_dst, user_item_src),
            ("item", "ib", "brand"): (item_brand_src, item_brand_dst),
            ("brand", "bi", "item"): (item_brand_dst, item_brand_src),
            ("item", "ic", "category"): (item_category_src, item_category_dst),
            ("category", "ci", "item"): (item_category_dst, item_category_src),
        }
    )

    print("Graph constructed.")

    # Split data into train/eval/test
    train_data, eval_data, test_data = split_data(hg, "ui")

    # delete the positive edges in eval/test data in the original graph
    train_pos = np.nonzero(train_data[:, 2])
    train_pos_idx = train_pos[0]
    user_item_src_processed = train_data[train_pos_idx, 0]
    user_item_dst_processed = train_data[train_pos_idx, 1]
    edges_dict = {
        ("item", "iv", "view"): (item_view_src, item_view_dst),
        ("view", "vi", "item"): (item_view_dst, item_view_src),
        ("user", "ui", "item"): (
            user_item_src_processed,
            user_item_dst_processed,
        ),
        ("item", "iu", "user"): (
            user_item_dst_processed,
            user_item_src_processed,
        ),
        ("item", "ib", "brand"): (item_brand_src, item_brand_dst),
        ("brand", "bi", "item"): (item_brand_dst, item_brand_src),
        ("item", "ic", "category"): (item_category_src, item_category_dst),
        ("category", "ci", "item"): (item_category_dst, item_category_src),
    }
    nodes_dict = {
        "user": hg.num_nodes("user"),
        "item": hg.num_nodes("item"),
        "view": hg.num_nodes("view"),
        "brand": hg.num_nodes("brand"),
        "category": hg.num_nodes("category"),
    }
    hg_processed = dgl.heterograph(
        data_dict=edges_dict, num_nodes_dict=nodes_dict
    )
    print("Graph processed.")

    # save the processed data
    with open(os.path.join(root_path, "amazon_hg.pkl"), "wb") as file:
        pkl.dump(hg_processed, file)
    with open(os.path.join(root_path, "amazon_train.pkl"), "wb") as file:
        pkl.dump(train_data, file)
    with open(os.path.join(root_path, "amazon_test.pkl"), "wb") as file:
        pkl.dump(test_data, file)
    with open(os.path.join(root_path, "amazon_eval.pkl"), "wb") as file:
        pkl.dump(eval_data, file)

    return hg_processed, train_data, eval_data, test_data


def process_movielens(root_path):
    # User-Movie 943 1682 100000 UMUM
    # User-Age 943 8 943 UAUM
    # User-Occupation 943 21 943 UOUM
    # Movie-Genre 1682 18 2861 UMGM

    data_path = os.path.join(root_path, "Movielens")
    if not (os.path.exists(data_path)):
        print(
            "Can not find movielens in {}, please download the dataset first.".format(
                data_path
            )
        )

    # Construct graph from raw data.
    # movie_genre
    movie_genre_src = []
    movie_genre_dst = []
    with open(os.path.join(data_path, "movie_genre.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split("\t")
            movie, genre = int(_line[0]), int(_line[1])
            movie_genre_src.append(movie)
            movie_genre_dst.append(genre)

    # user_movie
    user_movie_src = []
    user_movie_dst = []
    with open(os.path.join(data_path, "user_movie.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split("\t")
            user, item, rate = int(_line[0]), int(_line[1]), int(_line[2])
            if rate > 3:
                user_movie_src.append(user)
                user_movie_dst.append(item)

    # user_occupation
    user_occupation_src = []
    user_occupation_dst = []
    with open(os.path.join(data_path, "user_occupation.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split("\t")
            user, occupation = int(_line[0]), int(_line[1])
            user_occupation_src.append(user)
            user_occupation_dst.append(occupation)

    # user_age
    user_age_src = []
    user_age_dst = []
    with open(os.path.join(data_path, "user_age.dat")) as fin:
        for line in fin.readlines():
            _line = line.strip().split("\t")
            user, age = int(_line[0]), int(_line[1])
            user_age_src.append(user)
            user_age_dst.append(age)

    # build graph
    hg = dgl.heterograph(
        {
            ("movie", "mg", "genre"): (movie_genre_src, movie_genre_dst),
            ("genre", "gm", "movie"): (movie_genre_dst, movie_genre_src),
            ("user", "um", "movie"): (user_movie_src, user_movie_dst),
            ("movie", "mu", "user"): (user_movie_dst, user_movie_src),
            ("user", "uo", "occupation"): (
                user_occupation_src,
                user_occupation_dst,
            ),
            ("occupation", "ou", "user"): (
                user_occupation_dst,
                user_occupation_src,
            ),
            ("user", "ua", "age"): (user_age_src, user_age_dst),
            ("age", "au", "user"): (user_age_dst, user_age_src),
        }
    )

    print("Graph constructed.")

    # Split data into train/eval/test
    train_data, eval_data, test_data = split_data(hg, "um")

    # delete the positive edges in eval/test data in the original graph
    train_pos = np.nonzero(train_data[:, 2])
    train_pos_idx = train_pos[0]
    user_movie_src_processed = train_data[train_pos_idx, 0]
    user_movie_dst_processed = train_data[train_pos_idx, 1]
    edges_dict = {
        ("movie", "mg", "genre"): (movie_genre_src, movie_genre_dst),
        ("genre", "gm", "movie"): (movie_genre_dst, movie_genre_src),
        ("user", "um", "movie"): (
            user_movie_src_processed,
            user_movie_dst_processed,
        ),
        ("movie", "mu", "user"): (
            user_movie_dst_processed,
            user_movie_src_processed,
        ),
        ("user", "uo", "occupation"): (
            user_occupation_src,
            user_occupation_dst,
        ),
        ("occupation", "ou", "user"): (
            user_occupation_dst,
            user_occupation_src,
        ),
        ("user", "ua", "age"): (user_age_src, user_age_dst),
        ("age", "au", "user"): (user_age_dst, user_age_src),
    }
    nodes_dict = {
        "user": hg.num_nodes("user"),
        "movie": hg.num_nodes("movie"),
        "genre": hg.num_nodes("genre"),
        "occupation": hg.num_nodes("occupation"),
        "age": hg.num_nodes("age"),
    }
    hg_processed = dgl.heterograph(
        data_dict=edges_dict, num_nodes_dict=nodes_dict
    )
    print("Graph processed.")

    # save the processed data
    with open(os.path.join(root_path, "movielens_hg.pkl"), "wb") as file:
        pkl.dump(hg_processed, file)
    with open(os.path.join(root_path, "movielens_train.pkl"), "wb") as file:
        pkl.dump(train_data, file)
    with open(os.path.join(root_path, "movielens_test.pkl"), "wb") as file:
        pkl.dump(test_data, file)
    with open(os.path.join(root_path, "movielens_eval.pkl"), "wb") as file:
        pkl.dump(eval_data, file)

    return hg_processed, train_data, eval_data, test_data


class MyDataset(Dataset):
    def __init__(self, triple):
        self.triple = triple
        self.len = self.triple.shape[0]

    def __getitem__(self, index):
        return (
            self.triple[index, 0],
            self.triple[index, 1],
            self.triple[index, 2].float(),
        )

    def __len__(self):
        return self.len


def load_data(dataset, batch_size=128, num_workers=10, root_path="./data"):
    if os.path.exists(os.path.join(root_path, dataset + "_train.pkl")):
        g_file = open(os.path.join(root_path, dataset + "_hg.pkl"), "rb")
        hg = pkl.load(g_file)
        g_file.close()
        train_set_file = open(
            os.path.join(root_path, dataset + "_train.pkl"), "rb"
        )
        train_set = pkl.load(train_set_file)
        train_set_file.close()
        test_set_file = open(
            os.path.join(root_path, dataset + "_test.pkl"), "rb"
        )
        test_set = pkl.load(test_set_file)
        test_set_file.close()
        eval_set_file = open(
            os.path.join(root_path, dataset + "_eval.pkl"), "rb"
        )
        eval_set = pkl.load(eval_set_file)
        eval_set_file.close()
    else:
        if dataset == "movielens":
            hg, train_set, eval_set, test_set = process_movielens(root_path)
        elif dataset == "amazon":
            hg, train_set, eval_set, test_set = process_amazon(root_path)
        else:
            print("Available datasets: movielens, amazon.")
            raise NotImplementedError

    if dataset == "movielens":
        meta_paths = {
            "user": [["um", "mu"]],
            "movie": [["mu", "um"], ["mg", "gm"]],
        }
        user_key = "user"
        item_key = "movie"
    elif dataset == "amazon":
        meta_paths = {
            "user": [["ui", "iu"]],
            "item": [["iu", "ui"], ["ic", "ci"], ["ib", "bi"], ["iv", "vi"]],
        }
        user_key = "user"
        item_key = "item"
    else:
        print("Available datasets: movielens, amazon.")
        raise NotImplementedError

    train_set = torch.Tensor(train_set).long()
    eval_set = torch.Tensor(eval_set).long()
    test_set = torch.Tensor(test_set).long()

    train_set = MyDataset(train_set)
    train_loader = DataLoader(
        dataset=train_set,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers,
    )
    eval_set = MyDataset(eval_set)
    eval_loader = DataLoader(
        dataset=eval_set,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers,
    )
    test_set = MyDataset(test_set)
    test_loader = DataLoader(
        dataset=test_set,
        batch_size=batch_size,
        shuffle=True,
        num_workers=num_workers,
    )

    return (
        hg,
        train_loader,
        eval_loader,
        test_loader,
        meta_paths,
        user_key,
        item_key,
    )
